use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::Type; use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "user_role", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum UserRole { PlatformAdmin, ShopOwner, ShopStaff, Customer, } impl UserRole { pub fn is_shop_role(self) -> bool { matches!(self, UserRole::ShopOwner | UserRole::ShopStaff) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "shop_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ShopStatus { Active, Suspended, } #[derive(Debug, Clone, sqlx::FromRow)] pub struct User { pub id: Uuid, pub email: String, pub password_hash: String, pub display_name: String, pub role: UserRole, pub shop_id: Option, pub locale: String, pub created_at: DateTime, } /// Public user JSON. Never includes `password_hash`. #[derive(Debug, Serialize)] pub struct UserPublic { pub id: Uuid, pub email: String, pub display_name: String, pub role: UserRole, pub shop_id: Option, pub locale: String, pub created_at: DateTime, } impl From for UserPublic { fn from(u: User) -> Self { Self { id: u.id, email: u.email, display_name: u.display_name, role: u.role, shop_id: u.shop_id, locale: u.locale, created_at: u.created_at, } } } pub const USER_COLUMNS: &str = "id, email, password_hash, display_name, role, shop_id, locale, created_at"; #[derive(Debug, Serialize, sqlx::FromRow)] pub struct Shop { pub id: Uuid, pub name: serde_json::Value, pub slug: String, pub status: ShopStatus, pub created_at: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "product_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ProductStatus { Draft, Published, Unpublished, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Product { pub id: Uuid, pub shop_id: Uuid, pub category_id: Option, pub brand_id: Option, pub slug: String, pub name: serde_json::Value, pub description: serde_json::Value, pub images: serde_json::Value, pub status: ProductStatus, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Sku { pub id: Uuid, pub product_id: Uuid, pub sku_code: String, pub attributes: serde_json::Value, pub price_minor: i64, pub currency: String, pub stock: i32, /// Grams per unit for by-weight freight; null lines contribute zero weight. pub weight_grams: Option, pub active: bool, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct Currency { pub code: String, pub name: serde_json::Value, pub symbol: String, pub exponent: i16, pub is_base: bool, pub rate_to_base: rust_decimal::Decimal, pub enabled: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "order_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum OrderStatus { PendingPayment, Paid, Fulfilling, Shipped, Completed, Cancelled, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "shipment_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ShipmentStatus { Pending, Shipped, Delivered, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "invoice_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum InvoiceStatus { Requested, Issued, Cancelled, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "invoice_kind", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum InvoiceKind { Personal, Company, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Order { pub id: Uuid, pub order_no: String, pub shop_id: Uuid, pub user_id: Uuid, pub status: OrderStatus, pub currency: String, pub total_minor: i64, /// Realized coupon discount, converted to the order currency server-side. pub discount_minor: i64, /// Authoritative sum of completed after-sale refunds. pub refund_total_minor: i64, /// Server-computed per-shop-order shipping fee. pub shipping_fee_minor: i64, /// The coupon this order redeemed, if any. pub coupon_id: Option, /// Group-buying activity this order joined, if any. pub group_activity_id: Option, /// Concrete group this order belongs to, if any. pub group_id: Option, pub shipping_address: serde_json::Value, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct OrderItem { pub id: Uuid, pub order_id: Uuid, pub sku_id: Uuid, pub product_name: serde_json::Value, pub sku_code: String, pub image: Option, pub unit_price_minor: i64, pub qty: i32, /// Set when this line received flash-sale pricing. pub flash_sale_item_id: Option, /// Freight template + pricing method snapshotted at checkout. pub freight_template_id: Option, pub freight_pricing_method: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "aftersale_kind", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum AftersaleKind { RefundOnly, ReturnRefund, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "aftersale_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum AftersaleStatus { Pending, Approved, Rejected, BuyerShipping, MerchantConfirmed, Refunded, Cancelled, } impl AftersaleStatus { pub fn is_terminal(self) -> bool { matches!( self, AftersaleStatus::Refunded | AftersaleStatus::Rejected | AftersaleStatus::Cancelled ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "freight_pricing_method", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum FreightPricingMethod { ByPiece, ByWeight, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Shipment { pub id: Uuid, pub shipment_no: String, pub order_id: Uuid, pub carrier: String, pub tracking_no: String, pub status: ShipmentStatus, pub shipped_at: Option>, pub delivered_at: Option>, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct ShipmentItem { pub shipment_id: Uuid, pub order_item_id: Uuid, pub qty: i32, } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Invoice { pub id: Uuid, pub invoice_no: Option, pub order_id: Uuid, pub user_id: Uuid, pub title: String, pub tax_no: Option, pub kind: InvoiceKind, pub amount_minor: i64, pub currency: String, pub status: InvoiceStatus, pub issued_at: Option>, pub created_at: DateTime, } /// Balance bucket. `available`/`frozen` are monetary and carry a currency; /// `points` is an integer loyalty balance with no currency. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "customer_account_kind", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum AccountKind { Available, Frozen, Points, } impl AccountKind { pub fn is_monetary(self) -> bool { !matches!(self, AccountKind::Points) } } #[derive(Debug, Clone, sqlx::FromRow)] pub struct CustomerAccount { pub id: Uuid, pub user_id: Uuid, pub kind: AccountKind, pub currency: Option, pub balance_minor: i64, } pub const CUSTOMER_ACCOUNT_COLUMNS: &str = "id, user_id, kind, currency, balance_minor"; /// One immutable balance movement. Written in the same transaction as the /// guarded account update it describes. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct CustomerAccountEntry { pub id: Uuid, pub account_id: Uuid, pub delta_minor: i64, pub balance_minor: i64, pub reason: String, pub reference_type: Option, pub reference_id: Option, pub created_at: DateTime, } pub const CUSTOMER_ACCOUNT_ENTRY_COLUMNS: &str = "id, account_id, delta_minor, balance_minor, reason, reference_type, reference_id, created_at"; /// Lifecycle of a customer-owned coupon snapshot. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "coupon_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum CouponStatus { Claimed, Redeemed, Expired, } /// Shop-issued coupon definition. Editing one never changes claims already made. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct CouponTemplate { pub id: Uuid, pub shop_id: Uuid, pub title: serde_json::Value, pub amount_minor: i64, pub threshold_minor: i64, pub currency: String, /// Remaining claim stock; decremented conditionally on claim. pub stock: i32, pub enabled: bool, pub starts_at: DateTime, pub ends_at: DateTime, pub created_at: DateTime, } pub const COUPON_TEMPLATE_COLUMNS: &str = "id, shop_id, title, amount_minor, threshold_minor, \ currency, stock, enabled, starts_at, ends_at, created_at"; /// Customer-owned snapshot of a template's terms at claim time. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Coupon { pub id: Uuid, pub user_id: Uuid, /// Null once the issuing template is deleted; the snapshot still stands. pub template_id: Option, pub shop_id: Uuid, pub title: serde_json::Value, pub amount_minor: i64, pub threshold_minor: i64, pub currency: String, pub starts_at: DateTime, pub ends_at: DateTime, pub status: CouponStatus, pub order_id: Option, pub claimed_at: DateTime, pub redeemed_at: Option>, } 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, pub content: Option, pub image: Option, pub points_price: i64, pub stock: i32, pub published: bool, pub recommend: bool, pub position: i32, pub created_at: DateTime, pub updated_at: DateTime, } 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, pub updated_at: DateTime, } 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, pub name: serde_json::Value, pub image: Option, pub points_price: i64, pub qty: i32, } 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, pub ends_at: DateTime, pub enabled: bool, pub created_at: DateTime, pub updated_at: DateTime, } 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, pub updated_at: DateTime, } 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"; /// Lifecycle of a concrete group instance. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "collective_group_status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum CollectiveGroupStatus { Open, Successful, Expired, Cancelled, } /// Shop-owned timed activity on one SKU. Checkout accepts it at quantity 1 only. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct GroupBuyingActivity { pub id: Uuid, pub shop_id: Uuid, pub sku_id: Uuid, pub name: serde_json::Value, pub description: Option, pub image: Option, pub group_price_minor: i64, pub currency: String, pub required_members: i32, pub starts_at: DateTime, pub ends_at: DateTime, pub group_lifetime_hours: i32, pub enabled: bool, pub created_at: DateTime, pub updated_at: DateTime, } pub const GROUP_BUYING_ACTIVITY_COLUMNS: &str = "id, shop_id, sku_id, name, description, image, \ group_price_minor, currency, required_members, starts_at, ends_at, group_lifetime_hours, \ enabled, created_at, updated_at"; /// A concrete group instance. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct CollectiveGroup { pub id: Uuid, pub activity_id: Uuid, pub leader_order_id: Option, pub paid_member_count: i32, pub status: CollectiveGroupStatus, pub expires_at: DateTime, pub created_at: DateTime, pub updated_at: DateTime, } pub const COLLECTIVE_GROUP_COLUMNS: &str = "id, activity_id, leader_order_id, \ paid_member_count, status, expires_at, created_at, updated_at"; /// One paid seat: a membership row per paid order and group. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct CollectiveGroupMember { pub id: Uuid, pub group_id: Uuid, pub order_id: Uuid, pub user_id: Uuid, pub joined_at: DateTime, } pub const COLLECTIVE_GROUP_MEMBER_COLUMNS: &str = "id, group_id, order_id, user_id, joined_at"; #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct AddressBookEntry { pub id: Uuid, pub user_id: Uuid, pub recipient: String, pub phone: String, pub country: String, pub region: String, pub city: String, pub line1: String, pub postal_code: String, pub is_default: bool, pub created_at: DateTime, }