docs: architecture, guidelines, domain designs, progressive code index
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# Domain: After-sales and Reviews (post-order lifecycle)
|
||||
|
||||
## Aftersales (`modules/aftersale/`)
|
||||
|
||||
Per-order-item refund applications; the order's `refund_total_minor` is the
|
||||
authoritative sum of completed refunds.
|
||||
|
||||
```
|
||||
refund_only: pending ──approve──► approved ──refund──► refunded
|
||||
return_refund: pending ──approve──► approved ──buyer tracking──► buyer_shipping
|
||||
──confirm receipt──► merchant_confirmed ──refund──► refunded
|
||||
|
||||
pending ──reject──► rejected ──reopen (once)──► pending
|
||||
any non-terminal ──buyer cancel──► cancelled
|
||||
platform arbitration: pending ──► refunded | rejected (terminal)
|
||||
```
|
||||
|
||||
Invariants:
|
||||
|
||||
- One active (non-terminal) aftersale per order item — partial unique index.
|
||||
- Amount ≤ line paid − already refunded, checked at apply time.
|
||||
- Refund completion is one transaction: guarded status flip + guarded order
|
||||
`refund_total_minor` increment (`≤ total_minor`) + one ledger credit to the
|
||||
customer's available balance in the **order currency** (the account row is
|
||||
created lazily when missing). Retries hit the status guard — no double
|
||||
credit. The `refund_completed` hook point is a `tracing` call today;
|
||||
notifications attach there later.
|
||||
- Eligibility: order paid/fulfilling/shipped/completed and updated within
|
||||
`AFTERSALE_WINDOW_DAYS` (15).
|
||||
- Messages are an append-only bilateral log (buyer/merchant/platform); readers
|
||||
are the customer, the owning shop, and platform admins.
|
||||
|
||||
## Reviews (`modules/review/`)
|
||||
|
||||
One review per completed order line (unique index on `order_item_id`), rating
|
||||
1–5 + bilingual content (at least one locale; display falls back) + image
|
||||
URLs; immutable after creation. Merchant replies once (guarded
|
||||
`WHERE reply IS NULL`). Platform moderation: `visible` → `hidden` (guarded)
|
||||
or delete. Public listing and the SQL rating summary (count/avg/star
|
||||
distribution) only see `visible` rows. `GET /me/reviewable` lists the
|
||||
customer's completed lines without reviews.
|
||||
|
||||
## Key files
|
||||
|
||||
See `docs/code_index/post-order.md`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Domain: Catalog
|
||||
|
||||
Products, SKUs, categories, brands (`modules/product/`, `category/`, `brand/`).
|
||||
|
||||
## Shape
|
||||
|
||||
- `products`: shop-scoped, bilingual name/description, images JSONB array,
|
||||
status draft → published (publish requires ≥1 active SKU with price > 0),
|
||||
optional `freight_template_id` (must belong to the same shop).
|
||||
- `skus`: per-product purchasable units — code, attributes JSONB,
|
||||
`price_minor` + `currency`, guarded `stock`, `weight_grams` (freight).
|
||||
- `categories`: tree with subtree CTE for filtering; seeded reference tree.
|
||||
- `brands`: platform-managed ordered registry, whole-list replacement via
|
||||
`PUT /admin/brands`.
|
||||
|
||||
## Public reads
|
||||
|
||||
`GET /products` (published + active shop only; filters: category subtree,
|
||||
shop, name ILIKE, brand; sort newest/price/sales) and
|
||||
`GET /products/{idOrSlug}`. `sold_count` is computed from paid order items.
|
||||
Merchant reads/writes go through `/shop/products*` with `own_shop` scoping.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Every Product query uses explicit column lists — when adding a product
|
||||
column, update `PRODUCT_COLS` and the literal lists in `service.rs`
|
||||
(search: `category_id, brand_id, slug`).
|
||||
- SKU upsert is keyed on `(product_id, sku_code)`.
|
||||
- Stock is the only concurrently decremented counter today; always the
|
||||
guarded-decrement pattern.
|
||||
|
||||
## Key files
|
||||
|
||||
See `docs/code_index/catalog.md`.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Domain: Marketing (coupons, flash sales, group buying, points)
|
||||
|
||||
Four activity systems, all shop-scoped where merchant-facing, all resolved
|
||||
server-side at checkout.
|
||||
|
||||
## Coupons (`modules/coupon/`)
|
||||
|
||||
Merchant templates (amount/threshold/window/stock) → customers claim →
|
||||
redeem at checkout. Locked in id order during checkout; the discount is
|
||||
computed server-side in the order currency. Coupons never combine with
|
||||
activity pricing (flash sale / group buying) on the same shop order.
|
||||
|
||||
## Flash sales (`modules/flash_sale/`)
|
||||
|
||||
Merchant sessions with per-SKU items (activity price + reserved stock).
|
||||
Checkout splits a line into activity-priced and normal-priced units; activity
|
||||
stock is consumed via guarded decrement. A SKU rejects overlapping
|
||||
flash-sale and group-buying activities.
|
||||
|
||||
## Group buying (`modules/group_buying/`)
|
||||
|
||||
Merchant activity (required members, window, group lifetime). Checkout with a
|
||||
group intent takes exactly one unit at the activity price; a seat is claimed
|
||||
at payment (`paid_member_count` guarded), groups open/succeed/expire.
|
||||
|
||||
## Points mall (`modules/points/`)
|
||||
|
||||
Platform-curated catalog; customers redeem with the points account
|
||||
(append-only ledger, guarded points decrement), fulfillment/cancellation by
|
||||
platform admins.
|
||||
|
||||
## Shared rules
|
||||
|
||||
- All money integer minor units; activity prices convert to the order
|
||||
currency server-side.
|
||||
- All stock/seat counters use guarded `UPDATE … AND col >= $q`.
|
||||
- Mall exposes each domain as a mock-adapter-backed live domain
|
||||
(`LIVE_PICKS`); consoles are live-only.
|
||||
|
||||
## Key files
|
||||
|
||||
See `docs/code_index/marketing.md`.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Domain: Orders, Checkout, Fulfillment, Freight
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
cart → checkout (one tx) → order per shop shipment flow
|
||||
───────────────────────── ─────────────────────────
|
||||
pending_payment ──pay──► paid ──first shipment──► fulfilling ──all shipped──► shipped
|
||||
│ │
|
||||
└──cancel──► cancelled delivered (all shipments) ──► completed
|
||||
```
|
||||
|
||||
Every arrow is a guarded `UPDATE … WHERE status = …`; stale actions → 409.
|
||||
|
||||
## Checkout invariants (`modules/order/service.rs`)
|
||||
|
||||
One transaction per checkout: lock SKUs (`FOR UPDATE` in id order) → resolve
|
||||
flash-sale activity pricing → per-shop coupon discount (server-computed,
|
||||
client sends only coupon ids) → per-shop freight fee → insert order + items +
|
||||
stock decrements → commit → clear cart. Orders are split per shop; each order
|
||||
carries `total_minor = subtotal − discount + shipping_fee_minor`.
|
||||
|
||||
Snapshots make history immutable: order items keep `unit_price_minor`,
|
||||
`product_name`, `flash_sale_item_id`, and the freight
|
||||
`freight_template_id`/`freight_pricing_method` resolved at checkout. Later
|
||||
template edits never touch past orders.
|
||||
|
||||
## Freight (`modules/freight/`)
|
||||
|
||||
Templates are shop-scoped (`own_shop`): by_piece or by_weight (grams on
|
||||
`skus.weight_grams`), first-unit + additional-unit fees with part-unit
|
||||
round-up, `always_free`, `free_threshold_minor`, and region rules overriding
|
||||
default fees by case-insensitive match against address region/city/country.
|
||||
Resolution per line: product template → shop default → zero fee. Fees merge
|
||||
per template group within a shop order and convert from the group's SKU
|
||||
currency to the order currency.
|
||||
|
||||
`POST /orders/shipping-quote` is the read-only quote (normal prices only);
|
||||
checkout recomputes authoritatively in the order transaction. Client-supplied
|
||||
fees are never read.
|
||||
|
||||
Shipping companies are a dictionary (`shipping_companies`); shipments record
|
||||
`shipping_company_code` validated against it.
|
||||
|
||||
## Key files
|
||||
|
||||
See `docs/code_index/order.md`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Domain: Accounts, Content, Shops, Identity
|
||||
|
||||
## Customer accounts (`modules/account/`)
|
||||
|
||||
Three buckets per customer: `available` and `frozen` (monetary, per currency)
|
||||
plus `points` (no currency). Balances change only through
|
||||
`credit`/`debit`/`freeze`/`release`, each appending one immutable
|
||||
`customer_account_entries` row in the same transaction as the business write.
|
||||
The ledger is the audit trail; balances are never set absolutely. Credits in
|
||||
a currency the customer never held lazily create the zero row
|
||||
(`ensure_monetary_account`).
|
||||
|
||||
Planned on this foundation: wallet top-up/withdrawal and merchant settlement
|
||||
(`openspec/changes/add-wallet-settlement`).
|
||||
|
||||
## Storefront content (`modules/content/`)
|
||||
|
||||
Four ordered home-content kinds (banners, promos, quick links, floor
|
||||
adverts), platform-managed by whole-list replacement (`PUT
|
||||
/admin/content/{kind}` reindexes positions atomically). Quick-link glyphs are
|
||||
inline SVG path data.
|
||||
|
||||
## Shops (`modules/shop/`)
|
||||
|
||||
Platform creates/suspends shops; shop profiles are a side table with
|
||||
bilingual address/notice/after-sale and platform-owned scores. Merchants edit
|
||||
their own profile via `PUT /api/shop/profile` — scores are platform-only and
|
||||
the merchant upsert never touches them.
|
||||
|
||||
## Identity (`modules/identity/`, `src/auth.rs`)
|
||||
|
||||
Email+password register/login, JWT bearer tokens. `AuthUser` carries
|
||||
`id/role/shop_id`; `require(&[roles])` for RBAC, `own_shop()` /
|
||||
`require_shop()` for tenant scoping (cross-shop resources return 404).
|
||||
`ensure_accounts` runs inside the registration transaction.
|
||||
|
||||
## Key files
|
||||
|
||||
See `docs/code_index/platform.md`.
|
||||
Reference in New Issue
Block a user