feat(api): points mall with atomic redemption and admin fulfillment
Platform-owned points products and a redemption order lifecycle kept separate from cash orders. Redeeming locks the published product, reserves stock, creates the order, debits points through the archived customer-accounts ledger with the order as reference, and snapshots the line in one transaction; a failure leaves no order, no stock change, and no ledger entry. Fulfilment moves only from pending_fulfillment, and customers see only their own redemptions. Demo seeding credits the demo customer through the same guarded credit path with reason seed, once, so no balance is ever written absolutely. Surfaces (admin console, mall points page) and product seeding follow.
This commit is contained in:
@@ -343,6 +343,65 @@ pub struct Coupon {
|
||||
pub const COUPON_COLUMNS: &str = "id, user_id, template_id, shop_id, title, amount_minor, \
|
||||
threshold_minor, currency, starts_at, ends_at, status, order_id, claimed_at, redeemed_at";
|
||||
|
||||
/// Redemption lifecycle; separate from cash orders.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[sqlx(type_name = "integral_order_status", rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IntegralOrderStatus {
|
||||
PendingFulfillment,
|
||||
Fulfilled,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Platform-owned catalog item redeemable for points. Never a SKU.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct IntegralProduct {
|
||||
pub id: Uuid,
|
||||
pub name: serde_json::Value,
|
||||
pub subtitle: Option<serde_json::Value>,
|
||||
pub content: Option<serde_json::Value>,
|
||||
pub image: Option<String>,
|
||||
pub points_price: i64,
|
||||
pub stock: i32,
|
||||
pub published: bool,
|
||||
pub recommend: bool,
|
||||
pub position: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const INTEGRAL_PRODUCT_COLUMNS: &str = "id, name, subtitle, content, image, points_price, \
|
||||
stock, published, recommend, position, created_at, updated_at";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct IntegralOrder {
|
||||
pub id: Uuid,
|
||||
pub order_no: String,
|
||||
pub user_id: Uuid,
|
||||
pub status: IntegralOrderStatus,
|
||||
pub total_points: i64,
|
||||
pub shipping_address: serde_json::Value,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const INTEGRAL_ORDER_COLUMNS: &str =
|
||||
"id, order_no, user_id, status, total_points, shipping_address, created_at, updated_at";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct IntegralOrderItem {
|
||||
pub id: Uuid,
|
||||
pub order_id: Uuid,
|
||||
pub product_id: Option<Uuid>,
|
||||
pub name: serde_json::Value,
|
||||
pub image: Option<String>,
|
||||
pub points_price: i64,
|
||||
pub qty: i32,
|
||||
}
|
||||
|
||||
pub const INTEGRAL_ORDER_ITEM_COLUMNS: &str =
|
||||
"id, order_id, product_id, name, image, points_price, qty";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct AddressBookEntry {
|
||||
pub id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user