From 53548196f8856031732af51b821d433b71af02e4 Mon Sep 17 00:00:00 2001 From: Chengdong Zhang Date: Fri, 18 Sep 2026 17:32:08 +0800 Subject: [PATCH] docs(openspec): plan shop coupons, accounts, points, flash sales, and group buying Capture exclusive promotion composition, empty-group cancel, and mall surface scope so the five marketing changes can be implemented without stacking or lifecycle ambiguity. Co-authored-by: Cursor --- docs/TBD-marketing.md | 112 +++++++++++++----- .../add-customer-accounts/.openspec.yaml | 2 + .../changes/add-customer-accounts/design.md | 36 ++++++ .../changes/add-customer-accounts/proposal.md | 26 ++++ .../specs/customer-accounts/spec.md | 26 ++++ .../specs/frontend-mall/spec.md | 8 ++ .../changes/add-customer-accounts/tasks.md | 18 +++ .../changes/add-flash-sales/.openspec.yaml | 2 + openspec/changes/add-flash-sales/design.md | 40 +++++++ openspec/changes/add-flash-sales/proposal.md | 29 +++++ .../add-flash-sales/specs/flash-sales/spec.md | 26 ++++ .../specs/frontend-mall/spec.md | 16 +++ .../specs/frontend-shop-admin/spec.md | 12 ++ .../add-flash-sales/specs/order/spec.md | 39 ++++++ openspec/changes/add-flash-sales/tasks.md | 22 ++++ .../changes/add-group-buying/.openspec.yaml | 2 + openspec/changes/add-group-buying/design.md | 43 +++++++ openspec/changes/add-group-buying/proposal.md | 30 +++++ .../specs/frontend-mall/spec.md | 16 +++ .../specs/frontend-shop-admin/spec.md | 12 ++ .../specs/group-buying/spec.md | 34 ++++++ .../add-group-buying/specs/order/spec.md | 47 ++++++++ openspec/changes/add-group-buying/tasks.md | 22 ++++ .../changes/add-points-mall/.openspec.yaml | 2 + openspec/changes/add-points-mall/design.md | 33 ++++++ openspec/changes/add-points-mall/proposal.md | 28 +++++ .../specs/frontend-admin/spec.md | 12 ++ .../specs/frontend-mall/spec.md | 8 ++ .../add-points-mall/specs/points-mall/spec.md | 30 +++++ openspec/changes/add-points-mall/tasks.md | 21 ++++ .../changes/add-shop-coupons/.openspec.yaml | 2 + openspec/changes/add-shop-coupons/design.md | 36 ++++++ openspec/changes/add-shop-coupons/proposal.md | 29 +++++ .../specs/frontend-mall/spec.md | 16 +++ .../specs/frontend-shop-admin/spec.md | 12 ++ .../add-shop-coupons/specs/order/spec.md | 31 +++++ .../specs/shop-coupons/spec.md | 22 ++++ openspec/changes/add-shop-coupons/tasks.md | 23 ++++ 38 files changed, 894 insertions(+), 31 deletions(-) create mode 100644 openspec/changes/add-customer-accounts/.openspec.yaml create mode 100644 openspec/changes/add-customer-accounts/design.md create mode 100644 openspec/changes/add-customer-accounts/proposal.md create mode 100644 openspec/changes/add-customer-accounts/specs/customer-accounts/spec.md create mode 100644 openspec/changes/add-customer-accounts/specs/frontend-mall/spec.md create mode 100644 openspec/changes/add-customer-accounts/tasks.md create mode 100644 openspec/changes/add-flash-sales/.openspec.yaml create mode 100644 openspec/changes/add-flash-sales/design.md create mode 100644 openspec/changes/add-flash-sales/proposal.md create mode 100644 openspec/changes/add-flash-sales/specs/flash-sales/spec.md create mode 100644 openspec/changes/add-flash-sales/specs/frontend-mall/spec.md create mode 100644 openspec/changes/add-flash-sales/specs/frontend-shop-admin/spec.md create mode 100644 openspec/changes/add-flash-sales/specs/order/spec.md create mode 100644 openspec/changes/add-flash-sales/tasks.md create mode 100644 openspec/changes/add-group-buying/.openspec.yaml create mode 100644 openspec/changes/add-group-buying/design.md create mode 100644 openspec/changes/add-group-buying/proposal.md create mode 100644 openspec/changes/add-group-buying/specs/frontend-mall/spec.md create mode 100644 openspec/changes/add-group-buying/specs/frontend-shop-admin/spec.md create mode 100644 openspec/changes/add-group-buying/specs/group-buying/spec.md create mode 100644 openspec/changes/add-group-buying/specs/order/spec.md create mode 100644 openspec/changes/add-group-buying/tasks.md create mode 100644 openspec/changes/add-points-mall/.openspec.yaml create mode 100644 openspec/changes/add-points-mall/design.md create mode 100644 openspec/changes/add-points-mall/proposal.md create mode 100644 openspec/changes/add-points-mall/specs/frontend-admin/spec.md create mode 100644 openspec/changes/add-points-mall/specs/frontend-mall/spec.md create mode 100644 openspec/changes/add-points-mall/specs/points-mall/spec.md create mode 100644 openspec/changes/add-points-mall/tasks.md create mode 100644 openspec/changes/add-shop-coupons/.openspec.yaml create mode 100644 openspec/changes/add-shop-coupons/design.md create mode 100644 openspec/changes/add-shop-coupons/proposal.md create mode 100644 openspec/changes/add-shop-coupons/specs/frontend-mall/spec.md create mode 100644 openspec/changes/add-shop-coupons/specs/frontend-shop-admin/spec.md create mode 100644 openspec/changes/add-shop-coupons/specs/order/spec.md create mode 100644 openspec/changes/add-shop-coupons/specs/shop-coupons/spec.md create mode 100644 openspec/changes/add-shop-coupons/tasks.md diff --git a/docs/TBD-marketing.md b/docs/TBD-marketing.md index d5311b3..ac83c94 100644 --- a/docs/TBD-marketing.md +++ b/docs/TBD-marketing.md @@ -15,41 +15,65 @@ mock data and the page's `~/mock/data` import in the same change. --- +## Planning report — 2026-09-18 + +Five independent, implementation-ready OpenSpec changes were created and validated strictly: + +| Change | Business boundary | Prerequisite | +| --- | --- | --- | +| `add-shop-coupons` | Shop templates, customer claims, per-shop checkout redemption, and cancellation restoration | None | +| `add-customer-accounts` | Monetary/points accounts, append-only ledger, and live account stats | None | +| `add-points-mall` | Platform points catalog, atomic redemption, customer history, and admin fulfillment | `add-customer-accounts` must archive first | +| `add-flash-sales` | Timed sessions, SKU activity inventory, limits, and checkout pricing | None | +| `add-group-buying` | Activities, group lifecycle, paid membership, and payment-time capacity | None | + +Each change has `proposal.md`, `design.md`, `specs/`, and `tasks.md` in +`openspec/changes//`; all five passed `openspec change validate --strict`. +Their planning documents use generic B2B2C storefront terminology rather than a source-project +name. + +**Captured decisions:** coupons are template plus customer-owned snapshot and one per shop order; +activity-priced shop orders (flash or group) reject coupons; a SKU cannot be in overlapping flash +and group windows; account debits use guarded atomic updates with immutable entries; points +redemption uses a separate order lifecycle and demo points are seed-credited through the ledger; +flash sales use activity-reserved stock and server-calculated prices; group seats are claimed at +payment rather than checkout; an unpaid opener cancel closes an empty group; catalog product-detail +does not show flash or group prices in these changes. + +**Recommended implementation order:** start `add-customer-accounts` and/or +`add-shop-coupons`; archive accounts; then `add-points-mall`; implement `add-flash-sales` after +the coupon checkout shape is settled; implement `add-group-buying` last because it expands the +payment state machine. **Favorites** can be a separate OpenSpec change at any time (no order +coupling). The deferred designs below intentionally have no change yet. + ## Mock holdouts with a mall UI today -- [ ] **Coupons** — `user/coupons.vue` lists `MOCK_COUPONS`; `goods/[id].vue` shows a - claim strip off the same fixture. - Missing: `coupon_templates` (shop-issued: amount/threshold/window/stock), `coupons` - (user-held, order-bound status). APIs: `GET /api/coupons` (mine), - `POST /api/coupons/claim` (from a template), shop-admin template CRUD, and checkout - application (select → discount minor → bind to order). Money math stays minor units. +- [ ] **Coupons** — covered by in-progress `add-shop-coupons` (not yet applied). Mall still + reads `MOCK_COUPONS` until that change is implemented. - [ ] **Favorites** — `user/favorites.vue` lists `MOCK_FAVORITES` (products tab + - stores tab); `user/index.vue` derives counts from it. - Missing: `favorites(user_id, product_id | shop_id)` with a partial unique index per - target kind. APIs: `GET/POST/DELETE /api/favorites` (product & shop variants). -- [ ] **Account stats** — `user/index.vue` shows `USER_STATS` (balance 128.00, points - 2680, frozen 0) and `integral.vue` reuses the points figure. - Missing: balance/points accounts and ledgers (`money_logs` in the reference). - MVP shape: `GET /api/me/stats` returning `{ balance_minor, points, frozen_minor }`; - real ledgers only when a flow (recharge, refund-to-balance, points earn/spend) needs them. + stores tab); `user/index.vue` derives counts from it; `goods/[id].vue` heart is local + `ref(false)`. Ready for its own OpenSpec change whenever convenient: no checkout + coupling. Schema: `favorites(user_id, product_id | shop_id)` with a partial unique + index per target kind (see deferred design). APIs: `GET/POST/DELETE /api/favorites`. +- [ ] **Account stats** — covered by in-progress `add-customer-accounts` (not yet applied). + Mall still reads `USER_STATS` until that change lands. The change already includes + append-only ledgers and guarded mutation; public entry listing stays out of scope. + Frozen balance is a reserved kind and stays zero until a freeze flow exists. ## Marketing pages that are display-only mock These exist as full pages (`seckill.vue`, `collective.vue`, `integral.vue`) linked from the home navigation; all three read fixtures directly. -- [ ] **Seckill (秒杀)** — `SECKILL_SESSIONS` + `seckillProducts()` (price override, - sold %). - Missing: `seckill_sessions`, `seckill_products` (activity price, isolated stock), - `GET /api/seckill/sessions`, and checkout price resolution honouring the active session. -- [ ] **Collective / 拼团** — `collectiveProducts()` (need/joined counts). - Missing: `collective_activities`, `collective_groups` (open/join/expire, success on - fill); orders bind to a group; refund/rollback policy on expiry. -- [ ] **Integral mall / 积分商城** — `INTEGRAL_PRODUCTS` + points from `USER_STATS`. - Missing: points ledger (earn/spend), `integral_products`, points-denominated checkout - (`integral/orders` in the reference). +- [ ] **Seckill (秒杀)** — covered by in-progress `add-flash-sales`. Page still reads + `SECKILL_SESSIONS`. Catalog product-detail does not show flash prices in that change. +- [ ] **Collective / 拼团** — covered by in-progress `add-group-buying`. Page still reads + fixture counts. Paid seats at payment; unpaid opener cancel closes an empty group. +- [ ] **Integral mall / 积分商城** — covered by in-progress `add-points-mall` after + `add-customer-accounts` archives. Demo spendable points are seed-credited through the + ledger; earning campaigns stay out of that change. -## Domains the reference has and this MVP does not (no UI here) +## Domains typical B2B2C storefronts have and this MVP does not (no UI here) Recorded so the gap is explicit, not because all of them belong in scope: @@ -58,15 +82,41 @@ Recorded so the gap is explicit, not because all of them belong in scope: Missing: `order_comments` (order-item bound, rated, replyable), public read on product pages, shop reply, admin moderation. Writing/moderating/displaying reviews is a feature with its own lifecycle. -- [ ] **Distribution / 分销**, **cashes / 提现**, **money logs** — qwshop user-center - modules; no mock, no UI, no model here. -- [ ] **Help center / articles** — nav links exist in the footer (`帮助中心`); no article - model. Cheap version: static content pages; full version: admin-managed articles. -- [ ] **OAuth login, SMS/captcha** — reference `users/oauth` + captcha plugin; here auth - is email+password only. +- [ ] **Distribution / 分销**, **cashes / 提现** — common B2B2C account modules; no mock, + no UI, no model here. Customer-account freeze/credit is the intended foundation. + Public money-log listing is not in `add-customer-accounts`. Deferred design notes below. +- [ ] **Help center / articles** — header nav `shell.nav.help` currently links to `/user`; + the footer has no help links. No article model. Cheap version: static locale pages; + full version: admin-managed articles. +- [ ] **OAuth login, SMS/captcha** — common storefront plugins; here auth is email+password only. - [ ] **Freight templates / 运费模板** — shop-side shipping-fee rules; checkout currently charges no shipping at all. +## Deferred designs from general B2B2C storefronts + +These patterns are recorded for later product discovery only. They are not approved scope, +have no OpenSpec change, and must be redesigned under VMall's minor-unit, JSONB, RBAC, and +transaction rules before implementation. + +- [ ] **Threshold reductions / 满减** — merchant rules need a shop scope, localized name, + threshold and reduction in one currency, active window, and an explicit best-eligible rule. + Coupon vs flash/group is already exclusive (reject coupon on activity-priced shop orders). + Keep 满减 independent from coupons until a stacking policy with those shop coupons is specified. +- [ ] **Favorites** — do not use a polymorphic `target_id`/type pair. Keep the existing + product-or-shop design with separate nullable foreign keys, a target-kind check, and one + partial unique index per target type so duplicate claims are impossible. This can be a + dedicated OpenSpec change; it is the smallest remaining mall mock with a UI. +- [ ] **Reviews / 评价** — a future review belongs to a fulfilled order item, not just a + product. Preserve an immutable rating/content snapshot, allow one shop reply, and make + public visibility and platform moderation explicit lifecycle states. +- [ ] **Distribution / 分销** — a future merchant activity may configure per-product levels, + but commissions must be created as order-item ledger entries and become payable only after + the order reaches its chosen settlement condition. Do not use floating commission rates or + mutate a balance without a ledger entry. +- [ ] **Withdrawals / 提现** — model a payout request with account, amount minor, fee minor, + destination snapshot, and `requested → approved | rejected | paid` transitions. Funds must + be frozen and released or debited atomically with the transition. + ## Deliberately out of scope — does not block deleting this file - **The fixed-data adapter itself** (`apps/mall/mock/api.ts`, `~/mock/data`): the diff --git a/openspec/changes/add-customer-accounts/.openspec.yaml b/openspec/changes/add-customer-accounts/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/add-customer-accounts/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/add-customer-accounts/design.md b/openspec/changes/add-customer-accounts/design.md new file mode 100644 index 0000000..8e081b5 --- /dev/null +++ b/openspec/changes/add-customer-accounts/design.md @@ -0,0 +1,36 @@ +## Context + +The buyer center shows fixed balance, frozen balance, and points. Later points redemption requires protected debits and audit history; mutable profile columns would not provide either. VMall supports multiple currencies, while points are integer units rather than money. + +## Goals / Non-Goals + +**Goals:** +- Provide customer account summaries and append-only balance history. +- Make every later debit, credit, freeze, and release transactionally safe. +- Replace fixture account statistics in the mall. + +**Non-Goals:** +- Funding, payment, withdrawal, refunds, settlement, point-earning campaigns, or a public ledger listing. + +## Decisions + +### Accounts are keyed by kind and currency +`customer_accounts` has one row per `(user_id, kind, currency)`, where monetary kinds (`available`, `frozen`) carry an ISO currency and `points` has no currency. `balance_minor BIGINT` holds money and integer points; constraints reject negative stored balances and invalid kind/currency pairs. Seed/new users receive the defined account rows with zero balances. + +### Entries are immutable audit facts +`customer_account_entries` records account ID, signed delta, resulting balance, reason code, optional reference type/ID, and timestamp. Entries are inserted in the same transaction after their guarded account update. No public API mutates an account in this change; a module service provides the internal mutation API for later capabilities. + +### Guarded mutation is the only write path +Debit/freeze uses a conditional update whose predicate proves sufficient balance, returning Conflict on zero rows. Credit/release uses atomic addition. The service locks all affected accounts in stable `(kind, currency)` order, applies the transfers, then appends entries. This avoids races and keeps available/frozen transfers balanced. + +### Mall reads a defined summary +`GET /api/me/stats` returns the customer’s configured display-currency `balance_minor`, `frozen_minor`, `points`, and monetary currency. The shared client and mall user center consume it; localization remains UI-owned. `frozen_minor` is usually zero in this change because no public flow freezes funds. There is no customer ledger page or `GET` entries API. + +### Demo credits belong to later spend flows +New and seeded customers receive zero balances. Granting spendable points is not a campaign in this change; `add-points-mall` SHALL credit demo points through this module’s guarded credit path (append-only entry) after this change is archived. Direct `SET balance` is forbidden. + +## Risks / Trade-offs + +- Multi-currency balances introduce selection rules; the initial public summary exposes the buyer’s configured currency, while APIs remain extensible for future wallets. +- An internal service, rather than public mutation endpoints, prevents granting balances without a business event but means points mall must depend on this archived change. +- Exposing frozen balance before any freeze flow exists can look like a missing feature; keep the kind so later withdrawals do not migrate schema, and keep the mall figure honest (zero until a freeze exists). \ No newline at end of file diff --git a/openspec/changes/add-customer-accounts/proposal.md b/openspec/changes/add-customer-accounts/proposal.md new file mode 100644 index 0000000..e25ee0e --- /dev/null +++ b/openspec/changes/add-customer-accounts/proposal.md @@ -0,0 +1,26 @@ +## Why + +Account statistics are fixed mall fixtures, and points redemption needs an auditable balance rather than mutable user fields. A customer account boundary establishes safe balances and append-only history before any points-spend flow is introduced. + +## What Changes + +- Add one customer account per supported balance: cash balance, frozen cash, and integer loyalty points. +- Add append-only account entries that record the resulting balance, signed delta, reason, reference type, and reference ID. +- Add an authenticated customer account summary endpoint and replace the account-stat fixture on the mall user center. +- Provide transactional service operations for later flows to credit, debit, freeze, and release funds or points with non-negative-balance checks. + +## Capabilities + +### New Capabilities +- `customer-accounts`: Customer balance and points accounts, immutable entries, account summary, and guarded balance mutations. + +### Modified Capabilities +- `frontend-mall`: User-center account statistics load through the shared API client. + +## Non-goals + +No top-up, payment-from-balance, withdrawal, refund-to-balance, points earning campaign, merchant settlement, or public account-entry listing is included. The frozen monetary kind is reserved for later freeze/release flows. + +## Impact + +Adds Rust account module and migrations, shared account types/API/locales, a mall live-data migration, and a reusable transactional dependency for the points mall. \ No newline at end of file diff --git a/openspec/changes/add-customer-accounts/specs/customer-accounts/spec.md b/openspec/changes/add-customer-accounts/specs/customer-accounts/spec.md new file mode 100644 index 0000000..fa2f3a9 --- /dev/null +++ b/openspec/changes/add-customer-accounts/specs/customer-accounts/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Customer account summary +Each customer SHALL have available monetary balance, frozen monetary balance, and loyalty-points accounts. An authenticated customer SHALL retrieve a summary containing balance minor, frozen balance minor, their monetary currency, and integer points. Public APIs SHALL NOT list or mutate account entries. + +#### Scenario: new customer summary +- **WHEN** a newly registered customer requests account stats +- **THEN** the API returns zero balances and zero points without relying on browser fixtures + +#### Scenario: frozen kind exists without a freeze flow +- **WHEN** a customer with no freeze events requests account stats +- **THEN** `frozen_minor` is zero and available balance is unchanged + +### Requirement: Append-only account entries +Every account balance change SHALL create an immutable entry with signed delta, resulting balance, reason, optional business reference, and timestamp in the same transaction. Account entries SHALL NOT be edited or deleted through public APIs. + +#### Scenario: debit records result +- **WHEN** an internal business flow debits points successfully +- **THEN** the points balance and one entry with the resulting balance commit together + +### Requirement: Guarded account mutation +A debit or freeze SHALL succeed only when the affected account has sufficient available balance. Concurrent changes SHALL use conditional atomic updates and return 409 rather than create a negative balance. + +#### Scenario: competing debits +- **WHEN** two debits together exceed a points balance +- **THEN** at most one debit succeeds and the final stored balance is non-negative \ No newline at end of file diff --git a/openspec/changes/add-customer-accounts/specs/frontend-mall/spec.md b/openspec/changes/add-customer-accounts/specs/frontend-mall/spec.md new file mode 100644 index 0000000..a2e10a0 --- /dev/null +++ b/openspec/changes/add-customer-accounts/specs/frontend-mall/spec.md @@ -0,0 +1,8 @@ +## ADDED Requirements + +### Requirement: Live customer account statistics +The buyer-center dashboard and points surfaces SHALL load the signed-in customer's balance, frozen balance, currency, and points from the shared account API rather than `USER_STATS`. + +#### Scenario: account summary reload +- **WHEN** a signed-in shopper reloads the buyer center +- **THEN** displayed account statistics come from the selected API adapter and survive browser state loss \ No newline at end of file diff --git a/openspec/changes/add-customer-accounts/tasks.md b/openspec/changes/add-customer-accounts/tasks.md new file mode 100644 index 0000000..724a2a3 --- /dev/null +++ b/openspec/changes/add-customer-accounts/tasks.md @@ -0,0 +1,18 @@ +## 1. Account persistence and mutation boundary + +- [ ] 1.1 Add additive migrations for customer accounts, account kinds/currencies, append-only entries, and required indexes/checks. +- [ ] 1.2 Initialize zero account rows for existing seeded users and every newly registered customer. Do not credit demo points here. +- [ ] 1.3 Implement the Rust account module with stable-order account locking, conditional non-negative debits/freezes, atomic credits/releases, and entry insertion. + +## 2. Summary contract and proof + +- [ ] 2.1 Add shared account-summary types, client method, localized UI strings, and fixed-data adapter compatibility. +- [ ] 2.2 Expose authenticated `GET /api/me/stats` through the account service without public balance mutation endpoints. +- [ ] 2.3 Add API integration tests for zero initialization, summary ownership, immutable entries, available-to-frozen transfer, and competing insufficient debits. + +## 3. Mall migration and verification + +- [ ] 3.1 Replace buyer-center and points-page `USER_STATS` reads with the shared account summary API. +- [ ] 3.2 Remove affected stats fixture imports while keeping the fixed-data adapter rollback path intact. +- [ ] 3.3 Seed and browser-smoke the live buyer account summary. +- [ ] 3.4 Run cargo test for vmall-api, the mall build, and strict validation for this OpenSpec change. \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/.openspec.yaml b/openspec/changes/add-flash-sales/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/add-flash-sales/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/add-flash-sales/design.md b/openspec/changes/add-flash-sales/design.md new file mode 100644 index 0000000..75b5648 --- /dev/null +++ b/openspec/changes/add-flash-sales/design.md @@ -0,0 +1,40 @@ +## Context + +The flash-sale page derives sessions, prices, and sell-through from fixed data. The current checkout locks SKU rows and snapshots their converted prices but has no activity inventory or promotion source. A flash sale must be resolved by the server at checkout. + +## Goals / Non-Goals + +**Goals:** +- Let shops publish timed sale sessions and SKU-level activity items. +- Expose live session discovery and authoritative flash pricing. +- Protect both normal SKU and reserved activity inventory during checkout. + +**Non-Goals:** +- Traffic queues, bot prevention, platform promotions, stacking coupons onto flash-priced shop orders, waitlists, and automated retry. + +## Decisions + +### Session and item are separate, shop-scoped records +`flash_sale_sessions` stores `shop_id`, localized label, active window, and enabled state. `flash_sale_items` references a session and an owned SKU, and stores fixed `sale_price_minor`, currency, reserved stock, per-customer limit, and sold count. The service rejects items outside their shop, invalid windows, inactive SKUs, overlapping active sale items for the same SKU, and SKUs that already have an overlapping active group-buying activity. + +### Checkout discovers eligibility from server state +The customer continues using cart checkout; no displayed price is trusted. For each cart SKU, checkout finds a currently active applicable item, converts its fixed price to the requested order currency, applies it only within the item’s limit, and snapshots final unit price plus nullable `flash_sale_item_id` on the order item. Standard quantities beyond the limit retain normal price; no cart can receive two activity prices for one SKU. + +### Two inventories decrement together +Checkout locks SKUs and candidate flash items in deterministic UUID order. Each activity decrement is conditional on sufficient reserved stock, and each normal SKU decrement is conditional on sufficient SKU stock, all in the existing transaction. Pending-payment cancellation restores both inventory sources based on the order-item activity reference. + +### Mall and merchant UI use the shared contract +Public APIs return sessions and live products with remaining stock/sell-through. Shop-admin manages only its own sessions and items and exposes a flash-sale nav entry. Mall replaces `SECKILL_SESSIONS` and derived fixtures. Shoppers add the SKU to the existing cart; checkout does not trust the listing price. Payment and order-detail views show the snapshotted unit price. Catalog product-detail keeps SKU prices in this change. + +### Exclusive composition with coupons and group buying +A SKU MUST NOT be an eligible flash-sale item and an eligible group-buying activity in overlapping windows; shop configuration and checkout both reject that overlap. If any line on a generated shop order applied flash pricing, checkout SHALL reject `coupon_by_shop` for that shop (409) even when `add-shop-coupons` is already archived. This `order` spec restates coupon persistence and pending-payment coupon restore so later archival does not erase them. + +### Pending payment holds both inventories +Checkout decrements SKU stock and reserved activity stock before payment. Unpaid orders can exhaust displayable stock until the customer cancels or the pending-payment window ends. This change accepts that hostage; it does not add payment pre-authorization or delayed stock decrement. + +## Risks / Trade-offs + +- A dual-price cart line is more complex than an activity-only purchase path but preserves existing checkout. +- Concurrent checkout is the critical risk; integration tests must cover competing final stock and customer limit races. +- Time-window evaluation is request-time SQL/UTC logic; no scheduler is needed to turn displays off. +- Pending-payment inventory lock is visible as early sell-out relative to paid demand. \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/proposal.md b/openspec/changes/add-flash-sales/proposal.md new file mode 100644 index 0000000..89979b4 --- /dev/null +++ b/openspec/changes/add-flash-sales/proposal.md @@ -0,0 +1,29 @@ +## Why + +The mall's flash-sale page is display-only and prices are fixture-derived. A real event must allocate stock and resolve activity pricing inside checkout, not trust a price shown by the client. + +## What Changes + +- Add shop-managed flash-sale sessions and activity products with localized display data, fixed minor-unit activity price, window, per-customer limit, and isolated activity stock. +- Add public session and product discovery APIs and replace the flash-sale page fixtures with live data. +- Make checkout resolve an active eligible activity server-side, atomically decrement activity stock with SKU stock, and snapshot the applied activity price on the order item. +- Compose exclusively with other promotions: a line MAY receive flash pricing or group-buy pricing, never both; a shop order that applied any flash price SHALL reject a coupon. Restate shop-coupon redemption and pending-payment coupon restore in this change's `order` spec so archival does not drop them. +- Add shop-admin session and activity-product management scoped to the authenticated shop; mall payment and order-detail views show the snapshotted activity price. + +## Capabilities + +### New Capabilities +- `flash-sales`: Timed shop flash-sale sessions, reserved activity inventory, eligibility, and public discovery. + +### Modified Capabilities +- `order`: Checkout resolves and snapshots active flash-sale pricing. +- `frontend-mall`: Flash-sale discovery and product purchase use live data. +- `frontend-shop-admin`: Shops manage their own flash-sale configuration. + +## Non-goals + +Queues, bot mitigation, platform-wide campaigns, waitlists, automatic retry after sell-out, and stacking coupons onto flash-priced shop orders are excluded. Overlapping group-buy on the same SKU is rejected rather than merged. + +## Impact + +Adds a Rust marketing module and migrations, shared contract/locales, mall and shop-admin screens, and checkout concurrency coverage. \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/specs/flash-sales/spec.md b/openspec/changes/add-flash-sales/specs/flash-sales/spec.md new file mode 100644 index 0000000..bf1dfb4 --- /dev/null +++ b/openspec/changes/add-flash-sales/specs/flash-sales/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Shop flash-sale configuration +Shop users SHALL manage only their own timed flash-sale sessions and SKU activity items. An item SHALL have a fixed minor-unit sale price, currency, reserved stock, per-customer limit, and belong to an active SKU owned by the session shop. The service SHALL reject a SKU that already has an overlapping active group-buying activity. + +#### Scenario: cross-shop SKU rejected +- **WHEN** a merchant adds another shop's SKU to a flash-sale session +- **THEN** the API rejects it without creating an activity item + +#### Scenario: overlapping group-buy SKU rejected +- **WHEN** a merchant adds a SKU that already has an overlapping active group-buying activity +- **THEN** the API rejects it without creating an activity item + +### Requirement: Public active flash-sale discovery +Customers SHALL list active sessions and their eligible products with current sale price, remaining activity stock, and sell-through information. Inactive, disabled, or elapsed items SHALL not be purchasable as flash-sale items. + +#### Scenario: window closes +- **WHEN** a session end time has passed +- **THEN** it is absent from active discovery and checkout applies the normal SKU price + +### Requirement: Flash-sale purchase limit and inventory +Checkout SHALL enforce the activity's remaining stock and each customer's purchase limit atomically with normal SKU stock. A successful order SHALL retain the applied activity item and final price snapshot. + +#### Scenario: quantity exceeds limit +- **WHEN** a customer checks out a flash-sale SKU above the remaining per-customer allowance +- **THEN** only eligible activity quantity receives the activity price and any other quantity follows normal availability and price \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/specs/frontend-mall/spec.md b/openspec/changes/add-flash-sales/specs/frontend-mall/spec.md new file mode 100644 index 0000000..0004494 --- /dev/null +++ b/openspec/changes/add-flash-sales/specs/frontend-mall/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Live flash-sale page +The mall SHALL render active flash-sale sessions and activity products from the shared API client, including authoritative activity price, remaining stock, and sell-through. It SHALL replace `SECKILL_SESSIONS` and derived fixed product data. Shoppers SHALL purchase through the existing cart and checkout; payment and order-detail views SHALL show the snapshotted activity unit price from the order payload. This change SHALL NOT add flash-sale prices, badges, or claim CTAs to the catalog product-detail page (`/goods/[id]`); that page keeps catalog SKU pricing. + +#### Scenario: choose live session +- **WHEN** a shopper selects an active flash-sale session +- **THEN** its live eligible products and countdown render without direct fixture imports + +#### Scenario: order shows flash snapshot +- **WHEN** checkout applies a flash-sale price +- **THEN** payment and order-detail views show that unit price from the order payload + +#### Scenario: product detail stays on catalog price +- **WHEN** a shopper opens a catalog product that is also in an active flash-sale session +- **THEN** the product-detail page still shows catalog SKU prices and does not require a flash-sale overlay in this change \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/specs/frontend-shop-admin/spec.md b/openspec/changes/add-flash-sales/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..28c6c5e --- /dev/null +++ b/openspec/changes/add-flash-sales/specs/frontend-shop-admin/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Merchant flash-sale management +Shop users SHALL manage only their flash-sale sessions and items in shop-admin, choosing their own SKU, active window, minor-unit sale price, reserved stock, and per-customer limit. Shop-admin SHALL expose a flash-sale navigation entry beside existing shop operations. + +#### Scenario: configure item +- **WHEN** a merchant creates an active valid flash-sale item for its SKU +- **THEN** it appears to customers through active flash-sale discovery + +#### Scenario: flash sales appear in shop navigation +- **WHEN** an authenticated shop user opens shop-admin +- **THEN** a flash-sale management entry is reachable without leaving the shop-scoped console \ No newline at end of file diff --git a/openspec/changes/add-flash-sales/specs/order/spec.md b/openspec/changes/add-flash-sales/specs/order/spec.md new file mode 100644 index 0000000..a7871db --- /dev/null +++ b/openspec/changes/add-flash-sales/specs/order/spec.md @@ -0,0 +1,39 @@ +## MODIFIED Requirements + +### Requirement: Checkout splits by shop +`POST /api/orders/checkout` SHALL create one order per distinct shop in the cart, in a single database transaction: stock decrement, order + item insert with price snapshots, cart clear, active flash-sale resolution, and requested coupon redemption when eligible. All amounts use the cart's SKU currencies converted into the buyer-chosen display currency at checkout time. For each cart line the server SHALL resolve an eligible active flash-sale item, enforce its remaining stock and customer limit, snapshot the final unit price and applied activity identity, and charge standard price for quantity not eligible for the activity. A SKU that is also eligible for group-buying in an overlapping window SHALL cause checkout to return 409. The request MAY select at most one owned coupon per generated shop order; the server SHALL validate ownership, shop, status, validity window, threshold, and converted amount, then persist `coupon_id`, `discount_minor`, and the post-discount `total_minor`. The client SHALL NOT provide a discount amount. If any line on that shop order applied flash-sale pricing, the server SHALL reject a coupon for that shop with 409. + +#### Scenario: two shops → two orders +- **WHEN** the cart contains SKUs from shops A and B +- **THEN** two orders are created, each with only its shop's items, and the cart is empty + +#### Scenario: insufficient stock +- **WHEN** any line's qty exceeds SKU stock or applicable flash-sale stock +- **THEN** the whole checkout returns 409 and no order is created and stock is unchanged + +#### Scenario: active flash price applied +- **WHEN** checkout contains a SKU with an active eligible flash-sale item +- **THEN** the order item records the server-calculated flash-sale price and activity identity + +#### Scenario: coupon applies to a shop without flash lines +- **WHEN** the customer selects an eligible shop coupon and that shop's generated order has no flash-priced lines +- **THEN** the order records that coupon and its server-calculated discount + +#### Scenario: coupon rejected on a flash-priced shop order +- **WHEN** the customer selects a coupon for a shop whose generated order applied flash-sale pricing on any line +- **THEN** checkout returns 409 and creates no orders + +### Requirement: Order lifecycle +Status transitions SHALL be: pending_payment → paid → fulfilling → shipped → completed; cancellable only from pending_payment, which MUST restore SKU stock, any reserved flash-sale activity stock consumed by the order, and a redeemed coupon to claimed status when the order has one. + +#### Scenario: cancel restores stock +- **WHEN** a customer cancels a pending_payment order +- **THEN** stock of each SKU and its applied flash-sale activity increases by the ordered qty and status is cancelled + +#### Scenario: cancel restores coupon +- **WHEN** a customer cancels a pending_payment order with a redeemed coupon +- **THEN** the coupon is detached from the order and becomes claimed + +#### Scenario: illegal transition rejected +- **WHEN** cancelling a paid order via the customer endpoint +- **THEN** the API returns 409 diff --git a/openspec/changes/add-flash-sales/tasks.md b/openspec/changes/add-flash-sales/tasks.md new file mode 100644 index 0000000..3c78f08 --- /dev/null +++ b/openspec/changes/add-flash-sales/tasks.md @@ -0,0 +1,22 @@ +## 1. Flash-sale domain and contract + +- [ ] 1.1 Add additive migrations for shop flash-sale sessions, SKU activity items, reserved stock, per-customer limits, and order-item activity snapshots. +- [ ] 1.2 Implement the Rust flash-sale module with shop-scoped session/item management and public active-session discovery. +- [ ] 1.3 Add shared flash-sale types, client methods, localized strings, and fixed-data adapter parity. + +## 2. Checkout price and inventory resolution + +- [ ] 2.1 Resolve active eligible activity items server-side during checkout and snapshot final line pricing and activity identity. Reject coupon selection on any shop order that applied flash pricing; restate coupon persistence for non-flash shop orders. +- [ ] 2.2 Conditionally decrement and restore activity inventory with SKU inventory in deterministic lock order; enforce the per-customer limit. Pending-payment cancellation restores both inventories and any redeemed coupon. +- [ ] 2.3 Add integration tests for inactive-window fallback, cross-shop configuration rejection, overlapping group-buy SKU rejection, final reserved-stock contention, customer limits, coupon rejection on flash-priced shop orders, coupon restore on cancel, and cancellation restoration. + +## 3. Merchant and mall surfaces + +- [ ] 3.1 Add shop-admin session and activity-item management and a flash-sale nav entry scoped to the authenticated shop. +- [ ] 3.2 Replace the mall flash-sale fixture page with active live sessions, products, price, stock, sell-through, and countdown; show snapshotted activity prices on payment and order-detail views. +- [ ] 3.3 Remove affected direct flash-sale fixture imports while retaining fixed-data adapter support. + +## 4. Verification and specification + +- [ ] 4.1 Seed an active deterministic flash sale and browser-smoke its discovery and checkout pricing path. +- [ ] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change. \ No newline at end of file diff --git a/openspec/changes/add-group-buying/.openspec.yaml b/openspec/changes/add-group-buying/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/add-group-buying/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/add-group-buying/design.md b/openspec/changes/add-group-buying/design.md new file mode 100644 index 0000000..f77a1a2 --- /dev/null +++ b/openspec/changes/add-group-buying/design.md @@ -0,0 +1,43 @@ +## Context + +The group-buying page presents fixed participant counts. Unlike a simple price rule, group buying combines activity configuration, a concrete group lifecycle, order payment, capacity, and expiry. Existing customer orders are cancellable only before payment. + +## Goals / Non-Goals + +**Goals:** +- Let a shop publish a timed SKU-level group-buying activity. +- Let customers open or join concrete groups without over-capacity races. +- Bind the final price and group identity to an order. + +**Non-Goals:** +- Refund execution, shipping holds, multi-SKU groups, referrals, platform-wide campaigns, stacking coupons onto group-priced shop orders, and overlapping flash-sale pricing on the same SKU. + +## Decisions + +### Activities and groups have separate state +`group_buying_activities` stores shop, SKU, localized presentation, group price minor/currency, required paid members, active window, and group lifetime. Each membership order MAY include the activity SKU at quantity 1 only. `collective_groups` references an activity and records leader order, paid-member count, expiry, and `open | successful | expired | cancelled` state. `collective_group_members` is one row per paid order and group, with a uniqueness constraint per order and customer membership policy. + +### Membership finalizes at payment, not cart checkout +Checkout accepts an optional group-buy intent for exactly one activity SKU at quantity 1 and snapshots the activity price plus group ID on the pending order. Opening creates an empty open group; joining references an open group. Unpaid orders do not occupy a paid seat. `pay` locks the group and membership rows, expires due groups first, conditionally claims a paid seat, and transitions the group to successful exactly when its paid count reaches the required count. A full, expired, or cancelled group leaves the order pending payment and returns Conflict. + +### Expiry is explicit and deterministic +Before any read, join, or payment, the service marks due open groups expired. Pending orders have no paid membership, so pending-payment cancellation restores SKU stock only and does not change paid-member counts. A successful group is composed only of paid orders. Expired paid groups are visibly marked for later refund handling rather than silently changing balances. + +### Empty groups close when their opener cancels unpaid +`leader_order_id` is the pending order that opened the group at checkout; it may later be cancelled. When that opening order is cancelled while `paid_member_count` is still zero, the group transitions `open → cancelled` in the same transaction as stock restore. Cancelled groups are absent from open-group discovery; later join checkout or payment against that group id returns 409. Other pending joiners on that group cannot pay and must cancel to restore their SKU stock. If any seat is already paid, cancelling a different pending order leaves the group `open` (or `successful`/`expired` as already defined). + +### Exclusive composition with flash sales and coupons +A SKU MUST NOT be an eligible group-buying activity and an eligible flash-sale item in overlapping windows. Checkout SHALL return 409 when group intent targets a SKU that would also receive flash pricing, or when `coupon_by_shop` is set for the shop order that carries group intent. This `order` spec restates flash-sale price snapshots, dual-inventory restore, and coupon redemption/restore for non-group shop orders in the same checkout so archival does not drop earlier clauses. + +### Pending payment holds SKU stock +Checkout decrements SKU stock when the pending order is created. Unpaid group orders can exhaust SKU stock without filling the group. This change accepts that hostage; it does not delay stock decrement until payment. + +### Scope and UI follow existing boundaries +Shop-admin CRUD is constrained by `own_shop` and exposes a group-buying nav entry. Mall lists live activities/open groups and chooses open versus join, then submits checkout intent. Payment and order-detail views show the snapshotted group price. Catalog product-detail stays on SKU prices in this change. The shared client is the only browser API contract. + +## Risks / Trade-offs + +- Paid groups that expire without reaching capacity need a later refund capability; this proposal records the state but does not execute refunds. +- Payment-path group locking expands a sensitive existing transaction and needs concurrent-seat tests. +- The one-SKU, quantity-1 intent restriction avoids ambiguous multi-shop/cart promotion semantics. +- Pending-payment inventory lock can make a group look stocked-out before seats are paid. \ No newline at end of file diff --git a/openspec/changes/add-group-buying/proposal.md b/openspec/changes/add-group-buying/proposal.md new file mode 100644 index 0000000..2436c42 --- /dev/null +++ b/openspec/changes/add-group-buying/proposal.md @@ -0,0 +1,30 @@ +## Why + +The group-buying page displays invented participant counts but no group can be opened, joined, completed, or expired. This capability requires a visible lifecycle tied to checkout intent and payment-time seat claims. + +## What Changes + +- Add shop-managed group-buying activities with localized presentation data, fixed minor-unit group price, required paid-member count, eligibility window, and a per-order item limit of quantity 1 for the activity SKU. +- Add concrete group instances that can be opened, joined atomically up to capacity, succeed on reaching capacity, or expire. +- Bind qualifying orders to a group instance at checkout, resolve group price server-side, and expose activity and open-group discovery to customers. +- Claim paid membership at payment, not at cart checkout. Pending-payment cancellation restores SKU stock only and does not roll back paid seats. If the cancelled order opened a group that still has zero paid members, that empty group becomes `cancelled` and is no longer joinable. Paid groups that expire without filling remain identifiable for a later refund workflow. +- Compose exclusively with other promotions: a SKU MUST NOT overlap an active flash sale; a shop order with group-buy intent SHALL reject a coupon. Restate coupon and flash-sale checkout/cancel clauses in this change's `order` spec. +- Replace the display-only mall fixture and add shop-admin activity management. + +## Capabilities + +### New Capabilities +- `group-buying`: Timed group-buying activities, group-instance lifecycle, joining, and public discovery. + +### Modified Capabilities +- `order`: Checkout validates group participation and snapshots group-buy pricing. +- `frontend-mall`: Group-buying pages use live activity and group state. +- `frontend-shop-admin`: Shops manage their own group-buying activities. + +## Non-goals + +Refund execution, shipping holds, multi-product groups, platform campaigns, referral incentives, stacking coupons onto group-priced shop orders, and overlapping flash-sale pricing on the same SKU are excluded. + +## Impact + +Adds Rust marketing state-machine module/migrations, shared contract/locales, mall and shop-admin routes, and lifecycle/concurrency integration tests. \ No newline at end of file diff --git a/openspec/changes/add-group-buying/specs/frontend-mall/spec.md b/openspec/changes/add-group-buying/specs/frontend-mall/spec.md new file mode 100644 index 0000000..eaec154 --- /dev/null +++ b/openspec/changes/add-group-buying/specs/frontend-mall/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Live group-buying page +The mall SHALL render active group-buying activities and open groups from the shared API client, including group price, required member count, current paid-member count, and expiry. It SHALL let an authenticated shopper choose to open or join a group and replace derived fixture counts. Checkout SHALL submit the open-or-join intent for a single activity SKU at quantity 1. Payment and order-detail views SHALL show the snapshotted group price. This change SHALL NOT add group price or join CTAs to the catalog product-detail page (`/goods/[id]`); opening and joining happen from the group-buying page and checkout intent. + +#### Scenario: open a group +- **WHEN** a shopper selects an active group-buying product and chooses to open a group +- **THEN** the checkout intent identifies the activity and requests a new group + +#### Scenario: order shows group snapshot +- **WHEN** checkout succeeds with group-buy intent +- **THEN** payment and order-detail views show the server group unit price from the order payload + +#### Scenario: product detail stays on catalog price +- **WHEN** a shopper opens a catalog product that also has an active group-buying activity +- **THEN** the product-detail page still shows catalog SKU prices and does not require a group-buy overlay in this change \ No newline at end of file diff --git a/openspec/changes/add-group-buying/specs/frontend-shop-admin/spec.md b/openspec/changes/add-group-buying/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..0fbd6b5 --- /dev/null +++ b/openspec/changes/add-group-buying/specs/frontend-shop-admin/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Merchant group-buying management +Shop users SHALL manage only their shop's group-buying activities in shop-admin, configuring an owned SKU, localized display content, minor-unit group price, required paid-member count, active window, and group lifetime. Shop-admin SHALL expose a group-buying navigation entry beside existing shop operations. + +#### Scenario: publish activity +- **WHEN** a merchant publishes a valid group-buying activity +- **THEN** shoppers can discover it during its active window + +#### Scenario: group buying appears in shop navigation +- **WHEN** an authenticated shop user opens shop-admin +- **THEN** a group-buying management entry is reachable without leaving the shop-scoped console \ No newline at end of file diff --git a/openspec/changes/add-group-buying/specs/group-buying/spec.md b/openspec/changes/add-group-buying/specs/group-buying/spec.md new file mode 100644 index 0000000..feb7ada --- /dev/null +++ b/openspec/changes/add-group-buying/specs/group-buying/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Shop group-buying activities +Shop users SHALL manage only their timed group-buying activities, each bound to one owned SKU with localized display content, fixed minor-unit group price, required paid-member count of at least two, group lifetime, and a per-order activity quantity of 1. The service SHALL reject a SKU that already has an overlapping active flash-sale item. + +#### Scenario: invalid participant target +- **WHEN** a merchant configures a group activity requiring fewer than two paid members +- **THEN** the API rejects the activity + +#### Scenario: overlapping flash-sale SKU rejected +- **WHEN** a merchant publishes a group activity for a SKU that already has an overlapping active flash-sale item +- **THEN** the API rejects the activity + +### Requirement: Customer group discovery and intent +Customers SHALL discover active group-buying activities and open groups. A customer MAY initiate checkout by opening a new group or selecting an open eligible group for exactly one activity SKU at quantity 1. + +#### Scenario: join an open group +- **WHEN** a shopper selects an active open group and checks out its activity SKU +- **THEN** the resulting pending-payment order identifies that group and snapshots the group price + +### Requirement: Group membership and lifecycle +Payment SHALL atomically claim a paid membership in an open unexpired group. A group SHALL become successful exactly when paid member count reaches its required count; it SHALL become expired when its lifetime ends before success; it SHALL become cancelled when its opening pending-payment order is cancelled while paid member count is zero. A full, expired, or cancelled group SHALL reject payment without changing order state. + +#### Scenario: concurrent final seat +- **WHEN** two pending group orders attempt payment for one final seat concurrently +- **THEN** exactly one payment succeeds and the group becomes successful + +#### Scenario: expired paid group +- **WHEN** an open group expires before reaching its required count +- **THEN** it is marked expired and its paid orders remain identifiable for a later refund flow + +#### Scenario: unpaid opener cancel closes an empty group +- **WHEN** the opening pending-payment order is cancelled and the group still has zero paid members +- **THEN** the group becomes cancelled, SKU stock is restored, and later join or payment for that group returns 409 \ No newline at end of file diff --git a/openspec/changes/add-group-buying/specs/order/spec.md b/openspec/changes/add-group-buying/specs/order/spec.md new file mode 100644 index 0000000..1cef01f --- /dev/null +++ b/openspec/changes/add-group-buying/specs/order/spec.md @@ -0,0 +1,47 @@ +## MODIFIED Requirements + +### Requirement: Checkout splits by shop +`POST /api/orders/checkout` SHALL create one order per distinct shop in the cart, in a single database transaction: stock decrement, order + item insert with price snapshots, cart clear, active flash-sale resolution when no group-buy intent applies to the SKU, requested group-buying intent validation, and requested coupon redemption when eligible. All amounts use the cart's SKU currencies converted into the buyer-chosen display currency at checkout time. A group-buy intent SHALL contain exactly one active activity SKU at quantity 1 and either an open group identifier or a request to open a group; the server SHALL snapshot the group price and group identity on the resulting pending-payment order. Group intent SHALL return 409 when that SKU is also eligible for an overlapping flash sale. For each remaining cart line without group intent the server SHALL resolve an eligible active flash-sale item as specified by flash-sales, snapshot the final unit price and activity identity, and charge standard price for ineligible quantity. The request MAY select at most one owned coupon per generated shop order; the server SHALL persist `coupon_id`, `discount_minor`, and post-discount `total_minor` only after ownership, shop, status, window, and threshold validation. The client SHALL NOT provide a discount amount. The server SHALL reject a coupon (409) for any shop order that applied flash-sale pricing or carries group-buy intent. + +#### Scenario: two shops → two orders +- **WHEN** the cart contains SKUs from shops A and B without group-buy intent +- **THEN** two orders are created, each with only its shop's items, and the cart is empty + +#### Scenario: insufficient stock +- **WHEN** any line's qty exceeds SKU stock or applicable flash-sale stock +- **THEN** the whole checkout returns 409 and no order is created and stock is unchanged + +#### Scenario: open a group at checkout +- **WHEN** a customer checks out one eligible group-buying SKU at quantity 1 with open-group intent +- **THEN** a pending-payment order and an open group identity are created with the server-resolved group price + +#### Scenario: coupon rejected on a group shop order +- **WHEN** the customer selects a coupon for the shop that carries group-buy intent +- **THEN** checkout returns 409 and creates no orders + +#### Scenario: coupon applies to a shop without activity prices +- **WHEN** the customer selects an eligible shop coupon and that shop's generated order has no flash-priced lines and no group-buy intent +- **THEN** the order records that coupon and its server-calculated discount + +### Requirement: Order lifecycle +Status transitions SHALL be: pending_payment → paid → fulfilling → shipped → completed; cancellable only from pending_payment, which MUST restore SKU stock, any reserved flash-sale activity stock consumed by the order, and a redeemed coupon to claimed status when the order has one. Pending-payment cancellation SHALL NOT change group paid-member counts. When the cancelled order opened the group and paid member count is zero, the group SHALL become cancelled. Payment of an order with group-buying intent SHALL also validate and atomically claim an open group seat; full, expired, or cancelled groups SHALL return 409 and leave the order pending_payment. + +#### Scenario: group payment wins final seat +- **WHEN** payment fills the final available group seat +- **THEN** the payment succeeds and the group state becomes successful + +#### Scenario: cancel restores stock without group rollback +- **WHEN** a customer cancels a pending_payment group-intent order +- **THEN** SKU stock increases by the ordered qty, status is cancelled, and no paid group membership is removed + +#### Scenario: unpaid opener cancel closes an empty group +- **WHEN** the opening pending_payment order is cancelled and the group has zero paid members +- **THEN** the group status becomes cancelled and a later payment against that group returns 409 + +#### Scenario: cancel restores coupon +- **WHEN** a customer cancels a pending_payment order with a redeemed coupon +- **THEN** the coupon is detached from the order and becomes claimed + +#### Scenario: illegal transition rejected +- **WHEN** cancelling a paid order via the customer endpoint +- **THEN** the API returns 409 diff --git a/openspec/changes/add-group-buying/tasks.md b/openspec/changes/add-group-buying/tasks.md new file mode 100644 index 0000000..282978d --- /dev/null +++ b/openspec/changes/add-group-buying/tasks.md @@ -0,0 +1,22 @@ +## 1. Group-buying state model and contract + +- [ ] 1.1 Add additive migrations for shop activities, concrete groups, paid memberships, expiry/state constraints, and order group-price snapshots. +- [ ] 1.2 Implement the Rust group-buying module with shop-scoped activity management, active discovery, deterministic expiry, and open-group queries. +- [ ] 1.3 Add shared group-buying types, checkout intent (single SKU, quantity 1), API-client methods, locales, and fixed-data adapter parity. + +## 2. Checkout and payment integration + +- [ ] 2.1 Validate a single-SKU quantity-1 group-buying intent during checkout, create or reference an open group, and snapshot group price and identity on the pending order. Reject overlapping flash eligibility and coupon selection on that shop order; restate coupon and flash handling for other shop orders in the same checkout. +- [ ] 2.2 Extend payment to atomically expire due groups, claim a paid seat, enforce capacity, and transition a filled group to successful. Pending-payment cancellation restores SKU stock only and does not roll back paid membership. Cancelling the opening unpaid order while paid count is zero marks the group cancelled. +- [ ] 2.3 Add integration tests for activity/shop validation, open versus join flow, quantity-1 enforcement, concurrent final-seat payment, full/expired/cancelled-group conflicts, coupon rejection on group shop orders, overlapping flash rejection, cancel-without-membership-rollback, unpaid-opener empty-group cancel, and visibility of expired paid groups. + +## 3. Merchant and customer surfaces + +- [ ] 3.1 Add shop-admin management for activity SKU, localized presentation, price, required count, active window, group lifetime, and a group-buying nav entry. +- [ ] 3.2 Replace the mall group-buying fixture page with live activities, open groups, paid-member counts, expiry, and open/join checkout intent; show snapshotted group prices on payment and order-detail views. +- [ ] 3.3 Remove affected direct group-buying fixture imports while retaining fixed-data adapter support. + +## 4. Verification and specification + +- [ ] 4.1 Seed an active deterministic group activity and browser-smoke discovery, opening, joining, and successful payment. +- [ ] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change. \ No newline at end of file diff --git a/openspec/changes/add-points-mall/.openspec.yaml b/openspec/changes/add-points-mall/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/add-points-mall/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/add-points-mall/design.md b/openspec/changes/add-points-mall/design.md new file mode 100644 index 0000000..19f332e --- /dev/null +++ b/openspec/changes/add-points-mall/design.md @@ -0,0 +1,33 @@ +## Context + +The Mall has a fixture-only points catalog and points balance. Customer accounts will provide an atomic, append-only points ledger. Cash orders, payment, and shipments have a separate lifecycle, so a redemption should not be represented as a zero-value normal order. + +## Goals / Non-Goals + +**Goals:** +- Offer live points products and a customer redemption flow. +- Atomically debit points, reserve product stock, and create an immutable redemption order. +- Give platform operators product and redemption-order management. + +**Non-Goals:** +- Point earning, cash co-payment, normal SKU inventory sharing, coupons, withdrawals, or full shipping integration. + +## Decisions + +### Points catalog is platform-owned and distinct +`integral_products` holds localized name/subtitle/content, images, integer `points_price`, stock, publish state, and sort/recommend fields. It is not a SKU and never joins merchant catalog inventory. Platform admins own CRUD; customers can read published products only. + +### Redemptions have their own order snapshot +`integral_orders` stores customer, order number, state, address JSON snapshot, total points, and timestamps. `integral_order_items` snapshots product identity, localized name, image, point price, and quantity. States are `pending_fulfillment`, `fulfilled`, and `cancelled`; cancellation policy is platform-only until a future refund flow defines customer-initiated returns. + +### Redeem atomically against accounts and stock +The service begins one transaction, locks the published product, conditionally decrements stock, debits the points account through customer-accounts, appends its entry with the redemption order reference, then inserts the order and line. Failures leave no order, stock change, or ledger entry. Point price and stock are never accepted from the client. + +### UI replaces fixture-only behavior +Mall loads points balance and catalog through the shared client, submits a product/quantity/address redemption, and lists the customer’s redemption history on the points mall (not a cash-order list). Admin manages products and updates fulfillment state. Seed/demo data credits enough points for at least one redemption via the customer-accounts service with reason `seed`; it never assigns `balance_minor` by absolute overwrite. + +## Risks / Trade-offs + +- Redemption fulfillment initially stores only an address snapshot and status; carrier tracking can be integrated later with a dedicated model. +- The prerequisite customer-accounts change must archive first. This preserves a small, testable accounting boundary rather than duplicating balance mutation here. +- Without a seed credit, live redemption cannot be demonstrated; seed is an operator fixture, not a customer earn API. \ No newline at end of file diff --git a/openspec/changes/add-points-mall/proposal.md b/openspec/changes/add-points-mall/proposal.md new file mode 100644 index 0000000..83eafa2 --- /dev/null +++ b/openspec/changes/add-points-mall/proposal.md @@ -0,0 +1,28 @@ +## Why + +The mall exposes a fixed points-store page and a fixed points balance. A real points mall needs a dedicated redemption order lifecycle and must consume the customer-account ledger rather than treat points as an unguarded profile field. + +## What Changes + +- Add platform-managed points products with localized catalog content, integer point price, stock, availability, and images. +- Add points redemption orders and immutable redemption-line snapshots, separate from cash orders and payments. +- Debit points and decrement redemption stock atomically; reject insufficient points, inactive products, and unavailable stock. +- Seed demo spendable points only through the archived customer-accounts credit path (immutable entry); do not treat seed as an earning campaign. +- Add customer browsing, redemption, and redemption-history APIs; add platform product and redemption-order management; replace mall fixtures. + +## Capabilities + +### New Capabilities +- `points-mall`: Points product catalog and atomic customer redemption orders. + +### Modified Capabilities +- `frontend-mall`: Points mall and balance surfaces use live APIs. +- `frontend-admin`: Platform operators manage points products and redemption fulfillment. + +## Non-goals + +This change does not grant points, accept cash co-payments, share normal SKU inventory, add coupons to redemptions, or add customer withdrawals. + +## Impact + +Depends on archived `customer-accounts`; adds Rust points module/migrations, shared contract/locales, mall and admin routes, and points-redemption integration tests. \ No newline at end of file diff --git a/openspec/changes/add-points-mall/specs/frontend-admin/spec.md b/openspec/changes/add-points-mall/specs/frontend-admin/spec.md new file mode 100644 index 0000000..51b6d4e --- /dev/null +++ b/openspec/changes/add-points-mall/specs/frontend-admin/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Platform points mall management +Platform admins SHALL manage points products and inspect and fulfill redemption orders through the admin console using the shared API contract. Admin SHALL expose points-mall navigation beside existing platform operations. + +#### Scenario: manage a redemption catalog item +- **WHEN** a platform admin publishes a configured points product +- **THEN** it becomes available in the customer points mall + +#### Scenario: points mall appears in admin navigation +- **WHEN** an authenticated platform admin opens the admin console +- **THEN** points-product and redemption-order management are reachable from the console nav \ No newline at end of file diff --git a/openspec/changes/add-points-mall/specs/frontend-mall/spec.md b/openspec/changes/add-points-mall/specs/frontend-mall/spec.md new file mode 100644 index 0000000..6c51a8c --- /dev/null +++ b/openspec/changes/add-points-mall/specs/frontend-mall/spec.md @@ -0,0 +1,8 @@ +## ADDED Requirements + +### Requirement: Live points mall +The mall SHALL display live published points products and the signed-in customer's live points balance, allow redemption with a shipping address, and show the customer's redemption history on the points route. It SHALL replace `INTEGRAL_PRODUCTS` and `USER_STATS` imports on the points route. + +#### Scenario: redeem available product +- **WHEN** a signed-in shopper with enough points redeems a published in-stock product +- **THEN** the mall shows the created redemption order and refreshed points balance \ No newline at end of file diff --git a/openspec/changes/add-points-mall/specs/points-mall/spec.md b/openspec/changes/add-points-mall/specs/points-mall/spec.md new file mode 100644 index 0000000..a6e3662 --- /dev/null +++ b/openspec/changes/add-points-mall/specs/points-mall/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Platform points product catalog +Platform admins SHALL create, update, publish, unpublish, and list points products with localized user-facing content, integer points price, stock, images, and recommendation order. Customers SHALL discover only published products. + +#### Scenario: unpublished product is unavailable +- **WHEN** an admin unpublishes a points product +- **THEN** customers cannot discover or redeem it + +### Requirement: Atomic points redemption +An authenticated customer SHALL redeem a published points product using a quantity and shipping address. The server SHALL atomically validate stock, debit points through customer accounts, create a redemption order with immutable product and address snapshots, and decrement stock. + +#### Scenario: insufficient points +- **WHEN** a customer redeems a product costing more points than available +- **THEN** the API returns 409 and creates no redemption order or stock change + +#### Scenario: final stock contention +- **WHEN** concurrent redemptions request the final product stock +- **THEN** only requests covered by stock succeed and no negative stock is stored + +### Requirement: Redemption history and fulfillment +Customers SHALL list only their redemption orders. Platform admins SHALL list redemption orders and transition pending fulfillment to fulfilled or cancelled with validated current status. Demo seed SHALL credit points through customer accounts so at least one published product can be redeemed in the live mall. + +#### Scenario: fulfill redemption +- **WHEN** a platform admin fulfills a pending redemption order +- **THEN** its status becomes fulfilled and the customer sees that status in history + +#### Scenario: seeded points are spendable +- **WHEN** a seeded demo customer with a seed ledger credit redeems an in-stock published product they can afford +- **THEN** the redemption succeeds and the points summary decreases by the product price \ No newline at end of file diff --git a/openspec/changes/add-points-mall/tasks.md b/openspec/changes/add-points-mall/tasks.md new file mode 100644 index 0000000..4a30f22 --- /dev/null +++ b/openspec/changes/add-points-mall/tasks.md @@ -0,0 +1,21 @@ +## 1. Prerequisite and persistence + +- [ ] 1.1 Confirm `customer-accounts` is archived and use its guarded internal points-debit service. +- [ ] 1.2 Add additive migrations for platform points products, redemption orders, redemption items, status constraints, address snapshots, and indexes. +- [ ] 1.3 Implement the Rust points module repositories and services for published catalog reads, admin product management, atomic redemption, history, and validated fulfillment transitions. + +## 2. Contract and backend proof + +- [ ] 2.1 Register customer and platform routes with appropriate roles and add shared types, client methods, locales, and fixed-data adapter parity. +- [ ] 2.2 Add integration tests for unpublished-product rejection, insufficient points rollback, concurrent final stock, ownership filtering, and fulfillment transition validation. + +## 3. Application surfaces + +- [ ] 3.1 Add platform-admin points-product and redemption-order management screens and admin nav entries using the shared client. +- [ ] 3.2 Replace mall points fixtures with live catalog, address-aware redemption, refreshed balance, and customer redemption history. +- [ ] 3.3 Remove `INTEGRAL_PRODUCTS` and `USER_STATS` page imports without deleting fixed-data adapter support. + +## 4. Verification and specification + +- [ ] 4.1 Seed deterministic points products and credit demo customer points through the archived customer-accounts credit path (append-only entry, never an absolute balance write). Browser-smoke admin publication and customer redemption. +- [ ] 4.2 Run cargo test for vmall-api, builds for mall and admin, and strict validation for this OpenSpec change. \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/.openspec.yaml b/openspec/changes/add-shop-coupons/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/add-shop-coupons/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/add-shop-coupons/design.md b/openspec/changes/add-shop-coupons/design.md new file mode 100644 index 0000000..032473a --- /dev/null +++ b/openspec/changes/add-shop-coupons/design.md @@ -0,0 +1,36 @@ +## Context + +Checkout already splits a Redis cart into one database order per shop, converts SKU prices to the buyer-selected currency, and locks SKU rows. Mall coupon pages still read fixtures. The new capability crosses merchant management, customer claim/listing, and order creation. + +## Goals / Non-Goals + +**Goals:** +- Make shop coupons live, auditable, scoped, and safe under concurrent claims and checkout. +- Let a customer select no more than one coupon per generated shop order. +- Persist the realized minor-unit discount on the order. + +**Non-Goals:** +- Platform coupons, threshold reductions, customer targeting, or composing coupons with activity prices in this change. Later flash-sale and group-buying changes reject coupons on activity-priced shop orders. + +## Decisions + +### Coupon data is template plus owned snapshot +`coupon_templates` owns `shop_id`, localized title, amount/threshold minor, currency, claim stock, active window, and enabled state. `coupons` owns `user_id`, `template_id`, `shop_id`, copied terms/window, `status`, and nullable `order_id`. A unique `(user_id, template_id)` prevents duplicate claims. The snapshot preserves a claim if a template is edited or disabled later. + +### Checkout receives choices, not money +The shared checkout body adds `coupon_by_shop: Record`. The server rejects an unknown shop, a coupon not owned by the customer or shop, an unavailable or expired coupon, or an unmet threshold. It converts coupon amount and threshold to the buyer-selected order currency with the same enabled-currency conversion used for SKU prices. The server stores `coupon_id` and `discount_minor`; clients never submit a discount. + +### One transaction protects stock and redemption +Checkout locks SKU rows in UUID order, then selected coupon rows in UUID order. It conditionally decrements template claim stock on claim and moves a claimed coupon to redeemed only while all eligibility predicates hold. Pending-payment cancellation restores the coupon in the same transaction as SKU stock restoration. Expiry is evaluated at claim/list/checkout and recorded lazily; no scheduler is required. + +### Ownership and surfaces follow existing boundaries +Customer routes use `AuthUser`; shop routes require shop roles and `own_shop`. The contract remains exclusively in `@vmall/shared`; Mall replaces direct fixture imports and shop-admin gains template CRUD plus a nav entry. Checkout, payment, and order-detail surfaces show the server `discount_minor`; clients never invent it. + +### Later activities do not stack with coupons +This change is coupon-only: threshold and discount use the shop merchandise subtotal after catalog (or converted) line prices. When a later change introduces flash-sale or group-buy line prices, checkout MUST reject `coupon_by_shop` for any generated shop order that applied an activity price. Subsequent `order` spec deltas MUST copy this change's coupon validation, persistence, and pending-payment restore clauses instead of replacing the whole checkout requirement with activity-only text. + +## Risks / Trade-offs + +- Exchange-rate changes can change an unredeemed coupon's converted value; the realized `discount_minor` is immutable once ordered. Fixing value across currencies would need a new rate-snapshot policy. +- Activity-priced shop orders reject coupons rather than stack. Threshold reductions remain unspecified until a later change. +- Checkout contract expansion requires the fixed-data adapter to implement the same surface. \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/proposal.md b/openspec/changes/add-shop-coupons/proposal.md new file mode 100644 index 0000000..bad7669 --- /dev/null +++ b/openspec/changes/add-shop-coupons/proposal.md @@ -0,0 +1,29 @@ +## Why + +The mall displays fixed coupon fixtures although checkout has no promotion boundary. Shop-issued coupons are the smallest marketing capability that can use the existing per-shop checkout split without introducing a generic promotion engine. + +## What Changes + +- Add shop-scoped coupon templates with localized content, fixed minor-unit discount, threshold, currency, claim stock, active window, and lifecycle controls. +- Add customer coupon claiming, listing, and a user-held coupon snapshot with claimed/redeemed/expired state and order binding. +- Extend checkout to select at most one eligible coupon for each generated shop order; calculate and persist a server-authoritative discount. +- Record exclusive composition with later activity pricing: a shop order that applies flash-sale or group-buy prices MUST reject a coupon. Later order changes that rewrite checkout SHALL restate coupon redemption, cancellation restore, and this exclusion. +- Add shop-admin template management and replace the mall coupon fixtures with live data, checkout selection, and order-surface discount display. + +## Capabilities + +### New Capabilities +- `shop-coupons`: Shop coupon issuance, customer claims, eligibility, redemption, and cancellation restoration. + +### Modified Capabilities +- `order`: Checkout accepts per-shop coupon selections and persists the resulting discount. +- `frontend-mall`: Coupon surfaces and checkout use the shared live contract. +- `frontend-shop-admin`: Merchants manage coupons only within their own shop. + +## Non-goals + +Threshold reductions, stacking rules across promotion types, platform coupons, campaign targeting, and voucher transfers are excluded. Activity-priced checkout (flash sales, group buying) is out of this change; those later changes must reject coupons on activity-priced shop orders rather than leave stacking undefined. + +## Impact + +Adds Rust coupon module and migrations, shared types/API/locales, mall and shop-admin screens, and a checkout request/response contract change. \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/specs/frontend-mall/spec.md b/openspec/changes/add-shop-coupons/specs/frontend-mall/spec.md new file mode 100644 index 0000000..4eef994 --- /dev/null +++ b/openspec/changes/add-shop-coupons/specs/frontend-mall/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Live shop coupon surfaces +The mall SHALL load a product's currently claimable shop coupons and the signed-in customer's coupon list through the shared API client, replacing direct coupon fixtures. Checkout SHALL present eligible owned coupons separately for each shop order and show the server-returned discount after submission. Payment and order-detail views SHALL show the persisted `discount_minor` and post-discount total from the order payload. + +#### Scenario: claim from a product page +- **WHEN** a signed-in shopper claims an active coupon with remaining stock from a product page +- **THEN** it appears in the shopper's coupon list without a fixture import + +#### Scenario: select coupon by shop +- **WHEN** a cart spans two shops and the shopper selects a coupon for one shop +- **THEN** checkout submits the choice only for that shop and the other shop remains undiscounted + +#### Scenario: order shows realized discount +- **WHEN** checkout succeeds with a coupon on one shop order +- **THEN** that order's payment and detail views show the server discount and reduced total without a client-calculated amount \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/specs/frontend-shop-admin/spec.md b/openspec/changes/add-shop-coupons/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..75fb207 --- /dev/null +++ b/openspec/changes/add-shop-coupons/specs/frontend-shop-admin/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Merchant coupon management +Shop users SHALL manage only their shop's coupon templates from shop-admin, including localized title, amount and threshold in minor units, currency, claim stock, active window, and enabled state. Shop-admin SHALL expose a coupons navigation entry beside existing shop operations. + +#### Scenario: create an active template +- **WHEN** a shop owner creates a valid coupon template +- **THEN** the template is available for eligible customers to claim during its active window + +#### Scenario: coupons appear in shop navigation +- **WHEN** an authenticated shop user opens shop-admin +- **THEN** a coupons management entry is reachable without leaving the shop-scoped console \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/specs/order/spec.md b/openspec/changes/add-shop-coupons/specs/order/spec.md new file mode 100644 index 0000000..7521f29 --- /dev/null +++ b/openspec/changes/add-shop-coupons/specs/order/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Checkout splits by shop +`POST /api/orders/checkout` SHALL create one order per distinct shop in the cart, in a single database transaction: stock decrement, order + item insert with price snapshots, cart clear, and requested coupon redemption. All amounts use the cart's SKU currencies converted into the buyer-chosen display currency at checkout time. The request MAY select at most one owned coupon per generated shop order; the server SHALL validate its ownership, shop, status, validity window, threshold, and converted amount, then persist `coupon_id`, `discount_minor`, and the post-discount `total_minor`. The client SHALL NOT provide a discount amount. This requirement does not define activity prices; a later flash-sale or group-buying change that rewrites checkout SHALL restate these coupon clauses and SHALL reject a coupon on any shop order that applied an activity price. + +#### Scenario: two shops → two orders +- **WHEN** the cart contains SKUs from shops A and B +- **THEN** two orders are created, each with only its shop's items, and the cart is empty + +#### Scenario: insufficient stock +- **WHEN** any line's qty exceeds SKU stock +- **THEN** the whole checkout returns 409 and no order is created and stock is unchanged + +#### Scenario: coupon applies to one matching shop +- **WHEN** the customer selects an eligible coupon issued by shop A for a cart containing shops A and B +- **THEN** only shop A's order records that coupon and its server-calculated discount + +### Requirement: Order lifecycle +Status transitions SHALL be: pending_payment → paid → fulfilling → shipped → completed; cancellable only from pending_payment, which MUST restore stock and restore a redeemed coupon to claimed status when the order has one. + +#### Scenario: cancel restores stock +- **WHEN** a customer cancels a pending_payment order +- **THEN** stock of each SKU increases by the ordered qty and status is cancelled + +#### Scenario: cancel restores coupon +- **WHEN** a customer cancels a pending_payment order with a redeemed coupon +- **THEN** the coupon is detached from the order and becomes claimed + +#### Scenario: illegal transition rejected +- **WHEN** cancelling a paid order via the customer endpoint +- **THEN** the API returns 409 \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/specs/shop-coupons/spec.md b/openspec/changes/add-shop-coupons/specs/shop-coupons/spec.md new file mode 100644 index 0000000..ebe1ddf --- /dev/null +++ b/openspec/changes/add-shop-coupons/specs/shop-coupons/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: Shop coupon template management +A shop_owner or shop_staff SHALL create, list, update, disable, and delete only that shop's coupon templates. A template SHALL contain localized title, fixed `amount_minor`, `threshold_minor`, enabled ISO currency, non-negative claim stock, and an inclusive active window; amount MUST be positive and threshold MUST NOT be negative. + +#### Scenario: cross-shop template is hidden +- **WHEN** a shop user requests or mutates another shop's coupon template +- **THEN** the API returns 404 + +### Requirement: Customer coupon claim and listing +An authenticated customer SHALL list owned coupons and claim an active template at most once. Claiming SHALL atomically create a user-held snapshot and decrement claim stock only when stock remains; listing SHALL distinguish claimed, redeemed, and expired coupons. + +#### Scenario: concurrent final claim +- **WHEN** two customers claim the final remaining template instance concurrently +- **THEN** exactly one claim succeeds and the other receives 409 + +### Requirement: Coupon redemption lifecycle +A claimed coupon SHALL be redeemable only by its owner, for its issuing shop, within its snapshot window, and when the shop-order subtotal reaches its snapshot threshold. Redemption SHALL bind the coupon to one order; cancellation of that pending-payment order SHALL restore it to claimed. Later activity-priced shop orders (flash sale or group buy) SHALL NOT redeem a coupon; those changes restated this exclusion on checkout. + +#### Scenario: cancelled order restores coupon +- **WHEN** a customer cancels a pending-payment order redeemed with a coupon +- **THEN** the coupon again has claimed status and no order binding \ No newline at end of file diff --git a/openspec/changes/add-shop-coupons/tasks.md b/openspec/changes/add-shop-coupons/tasks.md new file mode 100644 index 0000000..fdf0962 --- /dev/null +++ b/openspec/changes/add-shop-coupons/tasks.md @@ -0,0 +1,23 @@ +## 1. Coupon domain and contract + +- [ ] 1.1 Add additive Postgres migrations for coupon templates, owned coupon snapshots, order coupon binding, and realized discount minor values. +- [ ] 1.2 Implement the Rust coupon module (DTOs, repository, service, handlers, router registration) with customer and shop RBAC/ownership boundaries. +- [ ] 1.3 Add `@vmall/shared` coupon types, API-client methods, localized UI strings, and matching fixed-data adapter methods. + +## 2. Atomic claims and checkout redemption + +- [ ] 2.1 Implement conditional template stock claim and per-customer duplicate protection with valid-window checks. +- [ ] 2.2 Extend checkout request handling with per-shop coupon choices, server-side currency conversion and threshold validation, and persisted discounts. +- [ ] 2.3 Restore redeemed coupons atomically with stock when a pending-payment order is cancelled. +- [ ] 2.4 Add API integration coverage for claim contention, cross-shop/expired/ineligible rejection, multi-shop redemption, and cancellation restoration. + +## 3. Merchant and customer surfaces + +- [ ] 3.1 Add shop-admin coupon-template management and a coupons nav entry scoped to the authenticated shop. +- [ ] 3.2 Replace mall product-detail coupon strip, buyer coupon list, and checkout coupon selection with shared live-client calls; show persisted discount on payment and order-detail views. +- [ ] 3.3 Remove the affected direct coupon fixture imports while retaining the complete fixed-data adapter rollback surface. + +## 4. Verification and specification + +- [ ] 4.1 Seed deterministic coupon templates and verify the live claim-to-checkout-to-cancel flow against the API. +- [ ] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change. \ No newline at end of file