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.
83 lines
2.2 KiB
Rust
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,
|
|
}
|