Files
vmall/apps/api/src/modules/group_buying/dto.rs
T
james a7bc476251 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.
2026-09-18 13:16:28 +00:00

83 lines
2.2 KiB
Rust

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,
}