Commit Graph
23 Commits
Author SHA1 Message Date
james 26773f7890 feat(shop-admin): manage flash-sale sessions and activity items
Add a flash-sale page where a shop user lists, creates, edits, and deletes its
own sessions and their SKU activity items, setting the fixed sale price,
currency, reserved stock, and per-customer limit. Expose it as a flash-sale
navigation entry beside the existing shop operations.
2026-09-18 12:53:26 +00:00
james 2125ae437f feat(mall): live flash-sale page and activity price snapshots
The flash-sale page loads active sessions and their items through the shared
client with live sale price, remaining activity stock, sell-through, and a
countdown, and adds the SKU to the existing cart so checkout resolves the
authoritative price. Payment and order detail tag and render the snapshotted
activity unit price.

The flash-sale fixtures leave the page; the fixed-data adapter still serves the
domain as the rollback path.
2026-09-18 12:53:26 +00:00
james 393e7f7209 feat(api): flash sales resolved server-side at checkout
Shop-owned timed sessions carry SKU activity items with their own reserved
inventory and per-customer limit. Checkout resolves eligibility on the server,
locks candidate items by primary key after the SKU locks, and splits a cart line
into an activity-priced item plus a standard-priced remainder, so every unit
price snapshot is honest and a customer cannot exceed the limit.

Reserved activity stock and SKU stock decrement together under guards, and a
pending-payment cancellation restores both plus any redeemed coupon. Coupons are
rejected on a shop order that applied activity pricing, and a SKU cannot join
two overlapping enabled sessions.

The overlap check against group buying is present but dormant: that capability
lands later, so the check activates only once its table exists.

Surfaces (shop-admin, mall) and seeding follow.
2026-09-18 12:47:46 +00:00
james 68317c2040 feat(admin): manage points products and redemptions
Platform operators get a points catalog screen (create, edit, publish and
unpublish products with localized names, points price, stock, and ordering) and
a redemption screen that lists orders and moves pending ones to fulfilled or
cancelled. Both are reachable from the console navigation.
2026-09-18 12:43:00 +00:00
james 059572f6f3 feat(mall): live points mall with address-aware redemption
The points page loads the published catalog and the customer's live points
through the shared client, redeems with a chosen saved address, refreshes the
balance, and lists the customer's redemption history with its fulfilment state.
The INTEGRAL_PRODUCTS and USER_STATS imports leave the page; the fixed-data
adapter still serves the points domain as the rollback path.
2026-09-18 12:43:00 +00:00
james 9c2b705880 feat(api): points mall with atomic redemption and admin fulfillment
Platform-owned points products and a redemption order lifecycle kept separate
from cash orders. Redeeming locks the published product, reserves stock, creates
the order, debits points through the archived customer-accounts ledger with the
order as reference, and snapshots the line in one transaction; a failure leaves
no order, no stock change, and no ledger entry. Fulfilment moves only from
pending_fulfillment, and customers see only their own redemptions.

Demo seeding credits the demo customer through the same guarded credit path
with reason seed, once, so no balance is ever written absolutely.

Surfaces (admin console, mall points page) and product seeding follow.
2026-09-18 12:24:46 +00:00
james c7fad42105 feat(shop-admin): manage shop coupon templates
Add a coupons page where a shop user lists, creates, edits, enables/disables,
and deletes only its own templates, with localized titles, minor-unit discount
and threshold, currency, claim stock, and an active window. Expose it as a
coupons navigation entry beside the existing shop operations.
2026-09-18 12:13:05 +00:00
james 0eb94f2ba1 feat(mall): claim and redeem shop coupons from the storefront
The product page loads a shop's claimable coupons through the shared client and
claims them in place; the buyer coupon list reads owned snapshots with their
claimed/redeemed/expired state. Checkout offers each shop order at most one
owned coupon and submits only the choice; the pay page and order detail render
the server-persisted discount and reduced total, never a client figure.

Coupon fixtures leave the pages; the fixed-data adapter still serves the coupon
domain as the rollback path. The demo seed creates two deterministic templates
and claims them for the demo customer.
2026-09-18 12:12:51 +00:00
james 23955434c6 feat(api): shop coupons with per-shop checkout redemption
Templates belong to a shop; claiming copies their terms into a customer-owned
snapshot so a later edit or disable cannot rewrite a held coupon. Claim stock
is taken with a guarded decrement after locking the template, and a unique
(user, template) index makes a duplicate claim a 409. Deleting a template
leaves claimed snapshots standing via ON DELETE SET NULL.

Checkout accepts at most one owned coupon per generated shop order, locks the
selected coupons by primary key after the SKU locks, and resolves eligibility
and the discount server-side (ownership, shop, status, window, converted
threshold). The realized discount and coupon id land on the order, and a
pending-payment cancellation restores the coupon in the same transaction as
stock.

The shared contract gains the coupon types, claim/list/manage methods, and the
checkout coupon map; the fixed-data adapter implements the same surface.

Surfaces (shop-admin management, mall coupon pages, checkout selection) and
seeding still follow in tasks 3.1-4.2.
2026-09-18 12:03:00 +00:00
james 7a2745fb16 feat(api): add customer accounts with live summary and append-only ledger
One balance row per (user, kind, currency): available and frozen carry the
platform base currency, points carries none. Monetary and points rows use
separate partial unique indexes because a plain UNIQUE lets NULL repeat.

Balance changes go through transactional primitives that debits guard with a
conditional update, credits add atomically, and freeze/release move both sides
in one transaction after locking rows by primary key. Every change appends an
immutable entry holding its resulting balance. Registration and a migration
backfill create the zero rows; GET /api/me/stats is the only public surface and
no endpoint mutates a balance.

The mall buyer center and points page drop the USER_STATS fixture for the
shared contract; the fixture stays exported so the fixed-data adapter can still
serve the account domain as a rollback path.

Implements openspec change add-customer-accounts.
2026-09-18 11:54:53 +00:00
Chengdong ZhangandCursor 835afbe5e0 fix(api): decrement SKU stock only when remaining quantity is sufficient
Prevent oversell by conditioning stock updates and locking SKUs in primary-key order, and record the same concurrent-counter rule in the API spec and agent guide.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 17:41:06 +08:00
Chengdong Zhang c51e96ae41 feat(mall): address book wave 7 frontend, contract, and archive
Live addresses domain for the mall: shared contract (AddressBookEntry +
five client methods), mock adapter state v3, live domain pick, addresses
page CRUD, checkout saved-address picker with manual fallback, demo seed,
and the archived change plus address-book capability spec.
2026-09-18 16:00:05 +08:00
Chengdong ZhangandCursor 5e866d08f4 refactor(api): split Axum handlers into handler/service/repo modules
Keep the REST contract; move domain logic out of route files so checkout, fulfillment, and identity can be reused across customer, shop, and admin surfaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 14:59:36 +08:00
james ce3e8db5b1 feat(mall): restore real brand and sales facets
Wave 6, the last substantive piece of the mock-API migration. Wave 1 removed the
brand facet and the sales/comments sorts for want of a model; sales turn out to
be derivable from order_items and a brand model is a table plus a column.

- a `brands` table with a nullable `products.brand_id` and an ordered admin
  replace, mirroring categories and storefront content; a public `GET /api/brands`
  and a `brand_id` filter on the catalog, which the search page's facet uses
- `sold_count` per product, computed from `order_items` joined to orders that
  reached payment, so an abandoned or cancelled checkout cannot count as a sale.
  It is computed per read rather than stored, so it cannot drift from the orders
  that produced it
- `sort=sales` alongside `sort=price`; anything else is still a 400
- merchants can set a product's brand through the existing product upsert
- the review UI is gone: the card's review figure and the product detail page's
  reviews tab, summary and replies. There is no reviews model, and the mall
  attributed invented comments to named shoppers and showed a "good rate". The
  now-unreferenced fabrication helpers went with it (`salesOf`, `commentCountOf`,
  `commentsFor`, `commentStats`, `salesRankFor`, `productDetail`, `storeDetail`)

Two bugs found by checking rather than trusting: the fixed-data `listProducts`
had silently ignored `brand_id`, `sort` and `order`, so the restored facet
rendered but filtered nothing until the rollback check caught it; and the seed's
brand lookup read back through the shared `r` variable the product loop
reassigns, working once and then throwing.

Verified: 29 backend tests green including a new brand-and-sales case; all three
frontends build; searching filters by brand (24 to 6) and sorts by sales with
counts matching the API; a product page offers detail and after-sale tabs only,
with a real sold count; the fixed-data rollback filters by brand too.

OpenSpec change: openspec/changes/replace-mock-api-wave-6
2026-09-17 17:35:28 +00:00
james d0b6350d2d feat(mall): read the store directory from the API
Wave 5: the store directory, store home and product-page store card stop reading
MOCK_STORES, and the payment and order surfaces name their shop.

- a `shop_profiles` table beside `shops`, so the identity model both consoles
  consume is untouched, with a public `GET /api/shops` and `GET /api/shops/{slug}`
  and an admin `PUT /api/admin/shops/{id}/profile`
- a shop with no profile is still listed, with the fields absent rather than
  invented; the pages guard every block, and a missing logo renders an
  initial-letter placeholder
- `scripts/seed-demo.mjs` upserts a profile per demo shop, since profiles hang
  off shops that script creates
- payment and order pages resolve shop ids to names from one cached shop read,
  retiring the generic "Shop" label
- three things went rather than being faked, following the wave-1 precedent:
  `distanceKm` and its sort (no geo model), the store home's sales/comments
  sorts, and its "best sellers" rail (no sales model)
- `lowestSku` moved out of the fixed-data module into `apps/mall/utils/product.ts`
  and re-exported, so live pages stop importing the mock module for a pure
  helper

Verified: 28 backend tests green including five new shop tests; all three
frontends build; the directory, store home, store card and order cards all render
real data with no distance or sales claims; the fixed-data rollback still renders
the store surfaces with the backend stopped.

Note: `nuxt build` does not typecheck in this repo (no `typescript.typeCheck`,
no `vue-tsc`), which AGENTS.md implies it does. A re-export used here created no
local binding and broke internal callers at runtime while the build stayed green;
`docs/TBD-migrate-wave.md` records the gap.

OpenSpec change: openspec/changes/replace-mock-api-wave-5
2026-09-17 17:13:32 +00:00
james 104737e4e1 feat(mall): serve home marketing content from the API
Wave 4 of replacing the fixed-data mock adapter, and the first capability the
mall never had a backend for: the home page's banners, promo tiles, quick links
and floor advert art move out of local arrays.

- four explicit tables (`banners`, `promos`, `quick_links`, `floor_adverts`)
  rather than one JSONB payload table, so Postgres enforces each shape
- a migration seeds them from the assets the page already rendered, so the flip
  is visually a no-op. Destinations are real routes now: the mock's promo links
  pointed at dangling `?category=c1` ids and its first banner used `sort=sales`,
  which the catalog API rejects
- `GET /api/content/home` is public and returns the four active, ordered lists,
  always including a key so a page can render a missing block
- `GET /api/admin/content` and `PUT /api/admin/content/{kind}` let a platform
  admin read everything and replace one kind transactionally, with positions
  reindexed from the submitted order and a rejected list changing nothing
- the mall's fixed-data adapter learns `getHomeContent`, and a `content` domain
  joins the per-domain switch so the rollback path still renders the page

Verified: 23 backend tests green including six new content tests; all three
frontends build; the home page renders the same four blocks as before, an admin
reorder and deactivation change the rendered carousel, and the fixed-data
rollback renders every block with the backend stopped.

OpenSpec change: openspec/changes/replace-mock-api-wave-4
2026-09-17 16:48:10 +00:00
james e1a0a5dbdb feat(mall): run the transaction chain against the live API
Wave 3 of replacing the fixed-data mock adapter: cart, orders, shipments and
invoices flip together, so one purchase runs end to end against the backend.

- cart: CartItemView carries the line's shop and the SKU's stock, so the cart
  keeps grouping per shop and the quantity stepper caps at real stock instead
  of a hard-coded 999
- contract: Shipment.items is optional and Invoice.invoice_no nullable, both
  matching what the API actually returns. Invoice was declared twice in
  types.ts and TypeScript merges duplicate interfaces, so the duplicate had to
  go for the change to take effect at all
- an anonymous add-to-cart redirects to /login?redirect=..., and sign-in
  honours only same-origin paths
- the fixed-data adapter learns the new cart fields, and its persisted state
  key moves to v2 because a cart saved by an older build is no longer valid
- order surfaces drop their storeById lookups and keep the generic store label
  until the public store read arrives

Verified end to end: two-shop cart grouping with live shop names, stock caps
read from the API, checkout, payment, shipment, delivery confirmation and an
issued invoice. Rollback re-verified with every domain on fixed data and the
backend stopped.

Also checks off Wave 3 in docs/TBD-migrate-wave.md and re-points that file at
the mock content that remains.

OpenSpec change: openspec/changes/replace-mock-api-wave-3
2026-09-17 16:33:22 +00:00
james 0ceb4a2b25 feat(mall): authenticate against the live API
Wave 2 of replacing the fixed-data mock adapter: the auth domain joins the live
list, so credentials, roles and tokens belong to the real user.

- session: validate a restored token through /auth/me instead of trusting
  localStorage, clearing it on 401/403 but keeping it when the API is merely
  unreachable; the route guard now uses the validated session
- login: report a 401 as invalid credentials rather than a generic failure, and
  drop the 6-character client rule so the API owns the password policy
- register: raise the rule to the API's 8 characters, remove the
  verification-code field (its button only counted down and the value was never
  sent), and report a duplicate email (409) distinctly
- TopBar and the user profile render their session-dependent branch client-only:
  validating the session before hydration made those localStorage-backed
  branches report hydration mismatches the previous code did not

Verified against the running backend: wrong password rejected, real JWT issued,
/user reachable, a short password refused with no network call, duplicate email
reported, a tampered token cleared and bounced to sign-in, a stale token kept
when the API is down, and the fixed-data rollback still signs in with the
backend stopped.

Also re-cuts docs/TBD-migrate-wave.md: auth is done, and cart, orders,
shipments and invoices must move together, because the mock adapter keeps their
state in one shared object and a partial flip fails at checkout.

OpenSpec change: openspec/changes/replace-mock-api-wave-2
2026-09-17 16:13:16 +00:00
james e0e833d0e5 feat(mall): serve catalog and currency from the live API
Wave 1 of replacing the fixed-data mock adapter. The mall now selects its API
adapter per domain, with catalog and currency served live while auth, cart,
orders, shipments and invoices stay on fixed data.

Backend:
- seed the 6 x 2 x 2 category tree as reference data (migration 0005). The API
  exposes no category write route, so this cannot come from the seed script
- filter public product listing by the category subtree with a recursive CTE,
  matching the mock's existing behaviour instead of exact-match
- add sort=price with order=asc|desc, validated by hand so an unsupported value
  returns the project's ApiError 400 shape rather than axum's own rejection

Mall:
- replace the all-or-nothing mockApi boolean with a liveDomains list composed
  through a typed per-domain pick map
- source home floors, the category menu, search and product detail from the
  catalog API; banners, promos, quick links, store card and comment/coupon
  content stay local display-only content
- drop the brand facet and the sales/comments sorts: no backend model backs them
- fix salesOf/commentCountOf, which parsed digits out of the product id and so
  rendered "NaN sold" for live UUID ids; they now hash the id

Seed: 24 products across 4 shops, idempotent on re-run.

Note: the mall defaults to a live catalog, so pnpm dev:mall now expects the API
to be running; set NUXT_PUBLIC_LIVE_DOMAINS to an empty array for all-mock work.

OpenSpec change: openspec/changes/replace-mock-api-wave-1
2026-09-17 15:15:25 +00:00
james ee2f421709 feat(mall): pin home category menu beside hero carousel
Home hero becomes a 450px flex row with a pinned 240x450 category rail
(ShellCategoryMenu `pinned` mode) left of the 960px center-cropped
carousel; hovering a top-level category expands the mega-menu over the
carousel. The header no longer collapses on scroll, and its category
dropdown is now hover-only on non-home pages.

OpenSpec change: openspec/changes/pin-home-category-menu

BREAKING CHANGE: the header logo/search/cart bar no longer auto-hides
past 200px of scroll, and the home page no longer shows the header
category dropdown (it is pinned in the hero row instead).
2026-09-17 14:17:46 +00:00
Chengdong Zhang 21e99bb52b feat(mall): classic B2B2C PC storefront with fixed mock API layer
- Mock adapter implementing @vmall/shared ApiClient (localStorage-persisted
  cart/orders/invoices), runtime switch via mockApi flag (default on)
- Fixed bilingual mock catalog: 3-level categories, 24 products w/ SKUs,
  stores, brands, banners, floors, seckill/collective/integral, comments,
  coupons, addresses, seeded orders/shipments/invoices
- B2B2C mall shell: top bar, logo/search/cart header, dark nav + category
  mega-menu, value-prop footer, back-to-top; 1200px grid, #ca151e theme
- UI primitives replacing element-plus: carousel, pagination, breadcrumb,
  qty stepper, rating stars, modal, tabs, step bar, product card
- Pages: home floors, search (filters/sort/paging), goods detail (SKU
  picker, store rail, review tabs), cart -> checkout -> pay -> success,
  auth pages, user center (dashboard/orders/addresses/favorites/coupons/
  invoices), stores, seckill, collective, integral
- i18n split into per-domain locale modules (en+zh)
- OpenSpec change mall-pc-storefront-replica archived; all specs green
2026-09-17 19:13:29 +08:00
Chengdong Zhang 653f13161a feat: three nuxt frontends, demo seed, rounding + money-exponent + rate-cast fixes, archived specs 2026-09-17 13:34:38 +08:00
Chengdong Zhang dc9fd31c5e feat: backend MVP (auth/rbac, catalog, orders, fulfillment, invoices) + specs + scaffolds 2026-09-17 12:43:22 +08:00