feat(api): flash sales resolved server-side at checkout
Shop-owned timed sessions carry SKU activity items with their own reserved inventory and per-customer limit. Checkout resolves eligibility on the server, locks candidate items by primary key after the SKU locks, and splits a cart line into an activity-priced item plus a standard-priced remainder, so every unit price snapshot is honest and a customer cannot exceed the limit. Reserved activity stock and SKU stock decrement together under guards, and a pending-payment cancellation restores both plus any redeemed coupon. Coupons are rejected on a shop order that applied activity pricing, and a SKU cannot join two overlapping enabled sessions. The overlap check against group buying is present but dormant: that capability lands later, so the check activates only once its table exists. Surfaces (shop-admin, mall) and seeding follow.
This commit is contained in:
@@ -208,6 +208,8 @@ pub struct OrderItem {
|
||||
pub image: Option<String>,
|
||||
pub unit_price_minor: i64,
|
||||
pub qty: i32,
|
||||
/// Set when this line received flash-sale pricing.
|
||||
pub flash_sale_item_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
@@ -402,6 +404,40 @@ pub struct IntegralOrderItem {
|
||||
pub const INTEGRAL_ORDER_ITEM_COLUMNS: &str =
|
||||
"id, order_id, product_id, name, image, points_price, qty";
|
||||
|
||||
/// Shop-owned timed flash-sale session.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct FlashSaleSession {
|
||||
pub id: Uuid,
|
||||
pub shop_id: Uuid,
|
||||
pub label: serde_json::Value,
|
||||
pub starts_at: DateTime<Utc>,
|
||||
pub ends_at: DateTime<Utc>,
|
||||
pub enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const FLASH_SALE_SESSION_COLUMNS: &str =
|
||||
"id, shop_id, label, starts_at, ends_at, enabled, created_at, updated_at";
|
||||
|
||||
/// SKU-level activity item with its own reserved inventory and customer limit.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct FlashSaleItem {
|
||||
pub id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub sku_id: Uuid,
|
||||
pub sale_price_minor: i64,
|
||||
pub currency: String,
|
||||
pub reserved_stock: i32,
|
||||
pub sold_count: i32,
|
||||
pub per_customer_limit: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct AddressBookEntry {
|
||||
pub id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user