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.
5.0 KiB
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.
Column contract with the archived flash-sales guard: the activity columns MUST be named sku_id, enabled, starts_at, and ends_at. add-flash-sales ships a dormant overlap check that queries exactly those names once this table exists; any other naming turns that guard into a runtime error on flash-item creation. Both directions of the exclusion are then live: flash rejects a SKU with an overlapping activity, and this change rejects an activity for a SKU with an overlapping flash item.
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.