wip(freight): migration 0017 + model/column-list groundwork for add-freight-templates
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
CREATE TYPE freight_pricing_method AS ENUM ('by_piece', 'by_weight');
|
||||
|
||||
CREATE TABLE freight_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shop_id UUID NOT NULL REFERENCES shops (id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
always_free BOOLEAN NOT NULL DEFAULT false,
|
||||
pricing_method freight_pricing_method NOT NULL DEFAULT 'by_piece',
|
||||
first_fee_minor BIGINT NOT NULL DEFAULT 0 CHECK (first_fee_minor >= 0),
|
||||
first_unit INT NOT NULL DEFAULT 1 CHECK (first_unit > 0),
|
||||
additional_fee_minor BIGINT NOT NULL DEFAULT 0 CHECK (additional_fee_minor >= 0),
|
||||
additional_unit INT NOT NULL DEFAULT 1 CHECK (additional_unit > 0),
|
||||
free_threshold_minor BIGINT CHECK (free_threshold_minor > 0),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- At most one default template per shop.
|
||||
CREATE UNIQUE INDEX freight_templates_one_default
|
||||
ON freight_templates (shop_id) WHERE is_default;
|
||||
CREATE INDEX idx_freight_templates_shop ON freight_templates (shop_id);
|
||||
|
||||
CREATE TABLE freight_region_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_id UUID NOT NULL REFERENCES freight_templates (id) ON DELETE CASCADE,
|
||||
regions TEXT[] NOT NULL,
|
||||
first_fee_minor BIGINT NOT NULL CHECK (first_fee_minor >= 0),
|
||||
first_unit INT NOT NULL CHECK (first_unit > 0),
|
||||
additional_fee_minor BIGINT NOT NULL CHECK (additional_fee_minor >= 0),
|
||||
additional_unit INT NOT NULL CHECK (additional_unit > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_freight_region_rules_template ON freight_region_rules (template_id);
|
||||
|
||||
CREATE TABLE shipping_companies (
|
||||
code TEXT PRIMARY KEY,
|
||||
name JSONB NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
INSERT INTO shipping_companies (code, name) VALUES
|
||||
('sf-express', '{"en": "SF Express", "zh": "顺丰速运"}'),
|
||||
('zto', '{"en": "ZTO Express", "zh": "中通快递"}'),
|
||||
('yto', '{"en": "YTO Express", "zh": "圆通速递"}'),
|
||||
('ems', '{"en": "EMS", "zh": "中国邮政 EMS"}'),
|
||||
('ups', '{"en": "UPS", "zh": "联合包裹"}'),
|
||||
('fedex', '{"en": "FedEx", "zh": "联邦快递"}');
|
||||
|
||||
ALTER TABLE products
|
||||
ADD COLUMN freight_template_id UUID REFERENCES freight_templates (id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE skus
|
||||
ADD COLUMN weight_grams INT CHECK (weight_grams > 0);
|
||||
|
||||
ALTER TABLE orders
|
||||
ADD COLUMN shipping_fee_minor BIGINT NOT NULL DEFAULT 0 CHECK (shipping_fee_minor >= 0);
|
||||
|
||||
-- Checkout snapshots so later template edits never touch history.
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN freight_template_id UUID,
|
||||
ADD COLUMN freight_pricing_method freight_pricing_method;
|
||||
|
||||
ALTER TABLE shipments
|
||||
ADD COLUMN shipping_company_code TEXT REFERENCES shipping_companies (code);
|
||||
@@ -110,6 +110,8 @@ pub struct Sku {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -175,6 +177,8 @@ pub struct Order {
|
||||
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.
|
||||
@@ -198,6 +202,9 @@ pub struct OrderItem {
|
||||
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)]
|
||||
@@ -230,6 +237,14 @@ impl AftersaleStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -9,10 +9,11 @@ use crate::models::{Order, OrderItem, OrderStatus};
|
||||
use super::dto::{OrderScope, OrderView};
|
||||
|
||||
const ORDER_COLS: &str = "id, order_no, shop_id, user_id, status, currency, total_minor,
|
||||
discount_minor, refund_total_minor, coupon_id, group_activity_id, group_id, shipping_address,
|
||||
created_at, updated_at";
|
||||
discount_minor, refund_total_minor, shipping_fee_minor, coupon_id, group_activity_id,
|
||||
group_id, shipping_address, created_at, updated_at";
|
||||
const ORDER_ITEM_COLS: &str =
|
||||
"id, order_id, sku_id, product_name, sku_code, image, unit_price_minor, qty, flash_sale_item_id";
|
||||
"id, order_id, sku_id, product_name, sku_code, image, unit_price_minor, qty, flash_sale_item_id,
|
||||
freight_template_id, freight_pricing_method";
|
||||
|
||||
pub async fn attach_items(db: &PgPool, orders: Vec<Order>) -> ApiResult<Vec<OrderView>> {
|
||||
let ids: Vec<Uuid> = orders.iter().map(|o| o.id).collect();
|
||||
|
||||
@@ -68,4 +68,5 @@ pub struct SkuBody {
|
||||
pub currency: String,
|
||||
pub stock: i32,
|
||||
pub active: Option<bool>,
|
||||
pub weight_grams: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub async fn attach_skus(
|
||||
Vec::new()
|
||||
} else if public_only {
|
||||
sqlx::query_as::<_, Sku>(
|
||||
"SELECT id, product_id, sku_code, attributes, price_minor, currency, stock, active
|
||||
"SELECT id, product_id, sku_code, attributes, price_minor, currency, stock, weight_grams, active
|
||||
FROM skus WHERE product_id = ANY($1) AND active = TRUE ORDER BY sku_code",
|
||||
)
|
||||
.bind(&ids)
|
||||
@@ -34,7 +34,7 @@ pub async fn attach_skus(
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as::<_, Sku>(
|
||||
"SELECT id, product_id, sku_code, attributes, price_minor, currency, stock, active
|
||||
"SELECT id, product_id, sku_code, attributes, price_minor, currency, stock, weight_grams, active
|
||||
FROM skus WHERE product_id = ANY($1) ORDER BY sku_code",
|
||||
)
|
||||
.bind(&ids)
|
||||
|
||||
@@ -297,11 +297,11 @@ pub async fn upsert_sku(
|
||||
)));
|
||||
}
|
||||
Ok(sqlx::query_as::<_, Sku>(
|
||||
"INSERT INTO skus (product_id, sku_code, attributes, price_minor, currency, stock, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
"INSERT INTO skus (product_id, sku_code, attributes, price_minor, currency, stock, active, weight_grams)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (product_id, sku_code)
|
||||
DO UPDATE SET attributes = $3, price_minor = $4, currency = $5, stock = $6, active = $7
|
||||
RETURNING id, product_id, sku_code, attributes, price_minor, currency, stock, active",
|
||||
DO UPDATE SET attributes = $3, price_minor = $4, currency = $5, stock = $6, active = $7, weight_grams = $8
|
||||
RETURNING id, product_id, sku_code, attributes, price_minor, currency, stock, weight_grams, active",
|
||||
)
|
||||
.bind(product_id)
|
||||
.bind(body.sku_code.trim())
|
||||
@@ -310,6 +310,7 @@ pub async fn upsert_sku(
|
||||
.bind(¤cy)
|
||||
.bind(body.stock)
|
||||
.bind(body.active.unwrap_or(true))
|
||||
.bind(body.weight_grams)
|
||||
.fetch_one(&state.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user