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>
43 lines
4.5 KiB
Markdown
43 lines
4.5 KiB
Markdown
## 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. |