chore(openspec): archive add-shop-coupons
Sync the shop-coupons capability plus the order, frontend-mall, and frontend-shop-admin requirements into the main specs, and write the capability Purpose the archive leaves as a placeholder. Renaming the coupon scenario in the flash-sales and group-buying order deltas to the exact name now in the main order spec keeps those changes archivable, since a MODIFIED requirement replaces the whole block.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-18
|
||||
@@ -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<shop UUID, coupon UUID>`. 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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
+12
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,23 @@
|
||||
## 1. Coupon domain and contract
|
||||
|
||||
- [x] 1.1 Add additive Postgres migrations for coupon templates, owned coupon snapshots, order coupon binding, and realized discount minor values.
|
||||
- [x] 1.2 Implement the Rust coupon module (DTOs, repository, service, handlers, router registration) with customer and shop RBAC/ownership boundaries.
|
||||
- [x] 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
|
||||
|
||||
- [x] 2.1 Implement conditional template stock claim and per-customer duplicate protection with valid-window checks.
|
||||
- [x] 2.2 Extend checkout request handling with per-shop coupon choices, server-side currency conversion and threshold validation, and persisted discounts.
|
||||
- [x] 2.3 Restore redeemed coupons atomically with stock when a pending-payment order is cancelled.
|
||||
- [x] 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
|
||||
|
||||
- [x] 3.1 Add shop-admin coupon-template management and a coupons nav entry scoped to the authenticated shop.
|
||||
- [x] 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.
|
||||
- [x] 3.3 Remove the affected direct coupon fixture imports while retaining the complete fixed-data adapter rollback surface.
|
||||
|
||||
## 4. Verification and specification
|
||||
|
||||
- [x] 4.1 Seed deterministic coupon templates and verify the live claim-to-checkout-to-cancel flow against the API.
|
||||
- [x] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change.
|
||||
Reference in New Issue
Block a user