feat(api): group buying with payment-time seat claims
Shop-owned activities on one SKU, concrete groups with an open/successful/expired/cancelled lifecycle, and one paid membership row per paid order. Checkout accepts a single-SKU quantity-1 intent, snapshots the group price and identity on the pending order, and opens or references a group; a paid seat is claimed only at payment, which locks the group and fills it exactly at capacity. Pending-payment cancellation restores SKU stock only and never rolls back paid seats; an unpaid opener cancelling closes a still-empty group. Coupons are refused on a group shop order, and the flash-sale exclusion is now enforced in both directions because the activity table exists, which activates the guard add-flash-sales shipped dormant. The activity column names follow the contract recorded in this change's design. Surfaces (shop-admin, mall) and seeding follow.
This commit is contained in:
@@ -193,6 +193,10 @@ pub struct Order {
|
||||
pub discount_minor: i64,
|
||||
/// The coupon this order redeemed, if any.
|
||||
pub coupon_id: Option<Uuid>,
|
||||
/// Group-buying activity this order joined, if any.
|
||||
pub group_activity_id: Option<Uuid>,
|
||||
/// Concrete group this order belongs to, if any.
|
||||
pub group_id: Option<Uuid>,
|
||||
pub shipping_address: serde_json::Value,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
@@ -438,6 +442,70 @@ pub struct FlashSaleItem {
|
||||
pub const FLASH_SALE_ITEM_COLUMNS: &str = "id, session_id, sku_id, sale_price_minor, currency, \
|
||||
reserved_stock, sold_count, per_customer_limit, created_at, updated_at";
|
||||
|
||||
/// Lifecycle of a concrete group instance.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[sqlx(type_name = "collective_group_status", rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CollectiveGroupStatus {
|
||||
Open,
|
||||
Successful,
|
||||
Expired,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Shop-owned timed activity on one SKU. Checkout accepts it at quantity 1 only.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct GroupBuyingActivity {
|
||||
pub id: Uuid,
|
||||
pub shop_id: Uuid,
|
||||
pub sku_id: Uuid,
|
||||
pub name: serde_json::Value,
|
||||
pub description: Option<serde_json::Value>,
|
||||
pub image: Option<String>,
|
||||
pub group_price_minor: i64,
|
||||
pub currency: String,
|
||||
pub required_members: i32,
|
||||
pub starts_at: DateTime<Utc>,
|
||||
pub ends_at: DateTime<Utc>,
|
||||
pub group_lifetime_hours: i32,
|
||||
pub enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const GROUP_BUYING_ACTIVITY_COLUMNS: &str = "id, shop_id, sku_id, name, description, image, \
|
||||
group_price_minor, currency, required_members, starts_at, ends_at, group_lifetime_hours, \
|
||||
enabled, created_at, updated_at";
|
||||
|
||||
/// A concrete group instance.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct CollectiveGroup {
|
||||
pub id: Uuid,
|
||||
pub activity_id: Uuid,
|
||||
pub leader_order_id: Option<Uuid>,
|
||||
pub paid_member_count: i32,
|
||||
pub status: CollectiveGroupStatus,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const COLLECTIVE_GROUP_COLUMNS: &str = "id, activity_id, leader_order_id, \
|
||||
paid_member_count, status, expires_at, created_at, updated_at";
|
||||
|
||||
/// One paid seat: a membership row per paid order and group.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct CollectiveGroupMember {
|
||||
pub id: Uuid,
|
||||
pub group_id: Uuid,
|
||||
pub order_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub joined_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const COLLECTIVE_GROUP_MEMBER_COLUMNS: &str =
|
||||
"id, group_id, order_id, user_id, joined_at";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct AddressBookEntry {
|
||||
pub id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user