chore(openspec): archive add-flash-sales

Sync the flash-sales 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.

Restate the flash-price and coupon-exclusion scenarios in the group-buying
order delta, since its MODIFIED checkout block must carry the scenarios the main
order spec now has.
This commit is contained in:
2026-09-18 12:53:26 +00:00
parent b15a246c3f
commit c93bff28b0
13 changed files with 85 additions and 12 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-18
@@ -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 items 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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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 one matching shop
- **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
@@ -0,0 +1,22 @@
## 1. Flash-sale domain and contract
- [x] 1.1 Add additive migrations for shop flash-sale sessions, SKU activity items, reserved stock, per-customer limits, and order-item activity snapshots.
- [x] 1.2 Implement the Rust flash-sale module with shop-scoped session/item management and public active-session discovery.
- [x] 1.3 Add shared flash-sale types, client methods, localized strings, and fixed-data adapter parity.
## 2. Checkout price and inventory resolution
- [x] 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.
- [x] 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.
- [x] 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
- [x] 3.1 Add shop-admin session and activity-item management and a flash-sale nav entry scoped to the authenticated shop.
- [x] 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.
- [x] 3.3 Remove affected direct flash-sale fixture imports while retaining fixed-data adapter support.
## 4. Verification and specification
- [x] 4.1 Seed an active deterministic flash sale and browser-smoke its discovery and checkout pricing path.
- [x] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change.