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:
@@ -0,0 +1,82 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ActivityInput {
|
||||
pub sku_id: Uuid,
|
||||
pub name: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub description: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
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,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Checkout intent: one activity SKU at quantity 1, either joining an open
|
||||
/// group or asking to open a new one.
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
pub struct GroupBuyIntent {
|
||||
pub activity_id: Uuid,
|
||||
pub sku_id: Uuid,
|
||||
#[serde(default)]
|
||||
pub group_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct OpenGroupView {
|
||||
pub id: Uuid,
|
||||
pub paid_member_count: i32,
|
||||
pub required_members: i32,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Activity plus the SKU/catalog details both surfaces need, with its open
|
||||
/// groups for discovery.
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct ActivityView {
|
||||
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 sku_code: String,
|
||||
pub product_id: Uuid,
|
||||
pub product_slug: String,
|
||||
pub product_name: serde_json::Value,
|
||||
pub original_price_minor: i64,
|
||||
pub original_currency: String,
|
||||
#[sqlx(skip)]
|
||||
pub open_groups: Vec<OpenGroupView>,
|
||||
}
|
||||
|
||||
/// Outcome of validating a checkout intent.
|
||||
pub struct ResolvedIntent {
|
||||
pub activity_id: Uuid,
|
||||
pub group_id: Uuid,
|
||||
pub shop_id: Uuid,
|
||||
pub unit_price_minor: i64,
|
||||
/// True when checkout opened the group rather than joining one.
|
||||
pub opened: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user