552 lines
16 KiB
Rust
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>,
|
|
}
|