Files
vmall/apps/api/src/models.rs
T

552 lines
16 KiB
Rust

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<Uuid>,
pub locale: String,
pub created_at: DateTime<Utc>,
}
/// 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<Uuid>,
pub locale: String,
pub created_at: DateTime<Utc>,
}
impl From<User> 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<Utc>,
}
#[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<Uuid>,
pub brand_id: Option<Uuid>,
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<Utc>,
pub updated_at: DateTime<Utc>,
}
#[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<i32>,
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<Uuid>,
/// Group-buying activity this order joined, if any.
pub group_activity_id: Option<Uuid>,
/// Concrete group this order belongs to, if any.
pub group_id: Option<Uuid>,
pub shipping_address: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[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<String>,
pub unit_price_minor: i64,
pub qty: i32,
/// Set when this line received flash-sale pricing.
pub flash_sale_item_id: Option<Uuid>,
/// Freight template + pricing method snapshotted at checkout.
pub freight_template_id: Option<Uuid>,
pub freight_pricing_method: Option<FreightPricingMethod>,
}
#[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<DateTime<Utc>>,
pub delivered_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[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<String>,
pub order_id: Uuid,
pub user_id: Uuid,
pub title: String,
pub tax_no: Option<String>,
pub kind: InvoiceKind,
pub amount_minor: i64,
pub currency: String,
pub status: InvoiceStatus,
pub issued_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
/// 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<String>,
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<String>,
pub reference_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
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<Utc>,
pub ends_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
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<Uuid>,
pub shop_id: Uuid,
pub title: serde_json::Value,
pub amount_minor: i64,
pub threshold_minor: i64,
pub currency: String,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
pub status: CouponStatus,
pub order_id: Option<Uuid>,
pub claimed_at: DateTime<Utc>,
pub redeemed_at: Option<DateTime<Utc>>,
}
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";
/// 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";
/// 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<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 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<Uuid>,
pub paid_member_count: i32,
pub status: CollectiveGroupStatus,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
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<Utc>,
}
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<Utc>,
}