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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
e10cae5789
commit
53548196f8
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-18
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user