chore(openspec): archive add-group-buying

Sync the group-buying 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. This closes the last of the five
planned marketing changes.
This commit is contained in:
2026-09-18 13:22:47 +00:00
parent 48ec5b4443
commit cdfb1b06f2
12 changed files with 97 additions and 16 deletions
@@ -2,7 +2,7 @@
- [x] 1.1 Add additive migrations for shop activities, concrete groups, paid memberships, expiry/state constraints, and order group-price snapshots. - [x] 1.1 Add additive migrations for shop activities, concrete groups, paid memberships, expiry/state constraints, and order group-price snapshots.
- [x] 1.2 Implement the Rust group-buying module with shop-scoped activity management, active discovery, deterministic expiry, and open-group queries. - [x] 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. - [x] 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. Checkout and payment integration
@@ -12,11 +12,11 @@
## 3. Merchant and customer surfaces ## 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. - [x] 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. - [x] 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. - [x] 3.3 Remove affected direct group-buying fixture imports while retaining fixed-data adapter support.
## 4. Verification and specification ## 4. Verification and specification
- [ ] 4.1 Seed an active deterministic group activity and browser-smoke discovery, opening, joining, and successful payment. - [x] 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. - [x] 4.2 Run cargo test for vmall-api, builds for mall and shop-admin, and strict validation for this OpenSpec change.
+15
View File
@@ -196,3 +196,18 @@ The mall SHALL render active flash-sale sessions and activity products from the
#### Scenario: product detail stays on catalog price #### Scenario: product detail stays on catalog price
- **WHEN** a shopper opens a catalog product that is also in an active flash-sale session - **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 - **THEN** the product-detail page still shows catalog SKU prices and does not require a flash-sale overlay in this change
### 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
@@ -40,3 +40,14 @@ Shop users SHALL manage only their flash-sale sessions and items in shop-admin,
#### Scenario: flash sales appear in shop navigation #### Scenario: flash sales appear in shop navigation
- **WHEN** an authenticated shop user opens shop-admin - **WHEN** an authenticated shop user opens shop-admin
- **THEN** a flash-sale management entry is reachable without leaving the shop-scoped console - **THEN** a flash-sale management entry is reachable without leaving the shop-scoped console
### 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
+39
View File
@@ -0,0 +1,39 @@
# group-buying Specification
## Purpose
Let a shop run timed group-buying on one of its SKUs: a shopper opens a group or joins an open one, and the group succeeds once enough members have paid. A seat is claimed at payment rather than checkout, so unpaid orders never occupy one; a group expires when its lifetime ends, and cancelling the unpaid opener closes a still-empty group. Group pricing is resolved and snapshotted server-side, and it is mutually exclusive with flash-sale pricing and coupons on the same shop order.
## 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
+26 -10
View File
@@ -6,10 +6,10 @@ Checkout splitting a cart into per-shop orders, plus order lifecycle and ownersh
## Requirements ## Requirements
### Requirement: Checkout splits by shop ### 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. `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 #### Scenario: two shops → two orders
- **WHEN** the cart contains SKUs from shops A and B - **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 - **THEN** two orders are created, each with only its shop's items, and the cart is empty
#### Scenario: insufficient stock #### Scenario: insufficient stock
@@ -17,23 +17,39 @@ Checkout splitting a cart into per-shop orders, plus order lifecycle and ownersh
- **THEN** the whole checkout returns 409 and no order is created and stock is unchanged - **THEN** the whole checkout returns 409 and no order is created and stock is unchanged
#### Scenario: active flash price applied #### Scenario: active flash price applied
- **WHEN** checkout contains a SKU with an active eligible flash-sale item - **WHEN** checkout contains a SKU with an active eligible flash-sale item and no group-buy intent applies to it
- **THEN** the order item records the server-calculated flash-sale price and activity identity - **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 #### 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 - **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 - **THEN** checkout returns 409 and creates no orders
#### 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 one matching shop
- **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 ### 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. 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 #### Scenario: cancel restores stock
- **WHEN** a customer cancels a pending_payment order - **WHEN** a customer cancels a pending_payment group-intent order
- **THEN** stock of each SKU and its applied flash-sale activity increases by the ordered qty and status is cancelled - **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 #### Scenario: cancel restores coupon
- **WHEN** a customer cancels a pending_payment order with a redeemed coupon - **WHEN** a customer cancels a pending_payment order with a redeemed coupon