Rearranges the Rust backend by domain: `modules/catalog` bundled four
distinct concepts (Product, SKU, Category, Brand) behind one router
and one 392-line service file, unlike every other module in the
codebase which owns exactly one bounded aggregate.
Splits into `modules/product` (Product/SKU, publish lifecycle,
shop-scoped CRUD), `modules/category` (category tree, subtree query),
and `modules/brand` (brand list, admin replace-all). `Category`/`Brand`
move out of the shared `models.rs` into their owning modules;
`Product`/`Sku` stay since `favorite`/`flash_sale`/`group_buying`
reference them across modules. Purely internal restructuring — no
route, schema, or behavior changes.
Implements openspec change split-catalog-into-product-category-brand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Shop-owned activities on one SKU, concrete groups with an
open/successful/expired/cancelled lifecycle, and one paid membership row per
paid order. Checkout accepts a single-SKU quantity-1 intent, snapshots the group
price and identity on the pending order, and opens or references a group; a paid
seat is claimed only at payment, which locks the group and fills it exactly at
capacity.
Pending-payment cancellation restores SKU stock only and never rolls back paid
seats; an unpaid opener cancelling closes a still-empty group. Coupons are
refused on a group shop order, and the flash-sale exclusion is now enforced in
both directions because the activity table exists, which activates the guard
add-flash-sales shipped dormant. The activity column names follow the contract
recorded in this change's design.
Surfaces (shop-admin, mall) and seeding follow.
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.
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.
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.
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.
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>
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