Commit Graph
12 Commits
Author SHA1 Message Date
Chengdong Zhang 5b426486ac wip(freight): migration 0017 + model/column-list groundwork for add-freight-templates 2026-09-23 17:39:45 +08:00
Chengdong Zhang 2c2b54c21c feat(aftersale): per-line refund/return flow with ledger-backed completion (add-aftersale-refunds) 2026-09-23 16:56:30 +08:00
Chengdong ZhangandClaude Sonnet 5 8446650baf refactor(api): split catalog module into product, category, brand
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>
2026-09-22 16:07:44 +08:00
Chengdong ZhangandCursor a4ef6ca49e chore(api): apply rustfmt across modules and tests
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 18:52:52 +08:00
james a7bc476251 feat(api): group buying with payment-time seat claims
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.
2026-09-18 13:16:28 +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 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 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 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
Chengdong Zhang dc9fd31c5e feat: backend MVP (auth/rbac, catalog, orders, fulfillment, invoices) + specs + scaffolds 2026-09-17 12:43:22 +08:00