Files
vmall/openspec/changes/add-group-buying/design.md
T
Chengdong ZhangandCursor 53548196f8 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>
2026-09-18 17:32:08 +08:00

4.5 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.

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.