feat: backend MVP (auth/rbac, catalog, orders, fulfillment, invoices) + specs + scaffolds
This commit is contained in:
@@ -0,0 +1 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TYPE user_role AS ENUM ('platform_admin', 'shop_owner', 'shop_staff', 'customer');
|
||||
|
||||
CREATE TYPE shop_status AS ENUM ('active', 'suspended');
|
||||
|
||||
CREATE TABLE shops (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name JSONB NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
status shop_status NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
role user_role NOT NULL DEFAULT 'customer',
|
||||
shop_id UUID REFERENCES shops (id),
|
||||
locale TEXT NOT NULL DEFAULT 'en',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT shop_role_requires_shop CHECK (
|
||||
role IN ('customer', 'platform_admin')
|
||||
OR shop_id IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_shop ON users (shop_id);
|
||||
@@ -0,0 +1,65 @@
|
||||
CREATE TYPE product_status AS ENUM ('draft', 'published', 'unpublished');
|
||||
|
||||
-- rate_to_base: units of this currency per 1 unit of the base currency
|
||||
-- (USD base: CNY 7.2 means 1 USD = 7.2 CNY).
|
||||
CREATE TABLE currencies (
|
||||
code CHAR(3) PRIMARY KEY,
|
||||
name JSONB NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
exponent SMALLINT NOT NULL,
|
||||
is_base BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
rate_to_base NUMERIC(20, 8) NOT NULL CHECK (rate_to_base > 0),
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
INSERT INTO currencies (code, name, symbol, exponent, is_base, rate_to_base)
|
||||
VALUES
|
||||
('USD', '{"en": "US Dollar", "zh": "美元"}', '$', 2, TRUE, 1),
|
||||
('CNY', '{"en": "Chinese Yuan", "zh": "人民币"}', '¥', 2, FALSE, 7.2),
|
||||
('EUR', '{"en": "Euro", "zh": "欧元"}', '€', 2, FALSE, 0.92),
|
||||
('JPY', '{"en": "Japanese Yen", "zh": "日元"}', '¥', 0, FALSE, 150);
|
||||
|
||||
CREATE TABLE categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_id UUID REFERENCES categories (id),
|
||||
name JSONB NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
position INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO categories (name, slug, position)
|
||||
VALUES
|
||||
('{"en": "Electronics", "zh": "电子产品"}', 'electronics', 1),
|
||||
('{"en": "Fashion", "zh": "时尚服饰"}', 'fashion', 2),
|
||||
('{"en": "Home & Living", "zh": "家居生活"}', 'home-living', 3);
|
||||
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shop_id UUID NOT NULL REFERENCES shops (id),
|
||||
category_id UUID REFERENCES categories (id),
|
||||
slug TEXT NOT NULL,
|
||||
name JSONB NOT NULL,
|
||||
description JSONB NOT NULL DEFAULT '{}',
|
||||
images JSONB NOT NULL DEFAULT '[]',
|
||||
status product_status NOT NULL DEFAULT 'draft',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (shop_id, slug)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_products_status ON products (status);
|
||||
CREATE INDEX idx_products_category ON products (category_id);
|
||||
|
||||
CREATE TABLE skus (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products (id) ON DELETE CASCADE,
|
||||
sku_code TEXT NOT NULL,
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
price_minor BIGINT NOT NULL CHECK (price_minor >= 0),
|
||||
currency CHAR(3) NOT NULL REFERENCES currencies (code),
|
||||
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE (product_id, sku_code)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_skus_product ON skus (product_id);
|
||||
@@ -0,0 +1,89 @@
|
||||
CREATE TYPE order_status AS ENUM (
|
||||
'pending_payment',
|
||||
'paid',
|
||||
'fulfilling',
|
||||
'shipped',
|
||||
'completed',
|
||||
'cancelled'
|
||||
);
|
||||
|
||||
CREATE TYPE shipment_status AS ENUM ('pending', 'shipped', 'delivered');
|
||||
|
||||
CREATE TYPE invoice_status AS ENUM ('requested', 'issued', 'cancelled');
|
||||
|
||||
CREATE TYPE invoice_kind AS ENUM ('personal', 'company');
|
||||
|
||||
CREATE SEQUENCE order_no_seq;
|
||||
CREATE SEQUENCE shipment_no_seq;
|
||||
CREATE SEQUENCE invoice_no_seq;
|
||||
|
||||
CREATE TABLE orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_no TEXT NOT NULL UNIQUE,
|
||||
shop_id UUID NOT NULL REFERENCES shops (id),
|
||||
user_id UUID NOT NULL REFERENCES users (id),
|
||||
status order_status NOT NULL DEFAULT 'pending_payment',
|
||||
currency CHAR(3) NOT NULL REFERENCES currencies (code),
|
||||
total_minor BIGINT NOT NULL CHECK (total_minor >= 0),
|
||||
shipping_address JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_orders_user ON orders (user_id);
|
||||
CREATE INDEX idx_orders_shop ON orders (shop_id);
|
||||
|
||||
CREATE TABLE order_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
|
||||
sku_id UUID NOT NULL REFERENCES skus (id),
|
||||
product_name JSONB NOT NULL,
|
||||
sku_code TEXT NOT NULL,
|
||||
image TEXT,
|
||||
unit_price_minor BIGINT NOT NULL CHECK (unit_price_minor >= 0),
|
||||
qty INT NOT NULL CHECK (qty > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_order_items_order ON order_items (order_id);
|
||||
|
||||
CREATE TABLE shipments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shipment_no TEXT NOT NULL UNIQUE,
|
||||
order_id UUID NOT NULL REFERENCES orders (id),
|
||||
carrier TEXT NOT NULL,
|
||||
tracking_no TEXT NOT NULL,
|
||||
status shipment_status NOT NULL DEFAULT 'pending',
|
||||
shipped_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_shipments_order ON shipments (order_id);
|
||||
|
||||
CREATE TABLE shipment_items (
|
||||
shipment_id UUID NOT NULL REFERENCES shipments (id) ON DELETE CASCADE,
|
||||
order_item_id UUID NOT NULL REFERENCES order_items (id),
|
||||
qty INT NOT NULL CHECK (qty > 0),
|
||||
PRIMARY KEY (shipment_id, order_item_id)
|
||||
);
|
||||
|
||||
CREATE TABLE invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invoice_no TEXT UNIQUE,
|
||||
order_id UUID NOT NULL REFERENCES orders (id),
|
||||
user_id UUID NOT NULL REFERENCES users (id),
|
||||
title TEXT NOT NULL,
|
||||
tax_no TEXT,
|
||||
kind invoice_kind NOT NULL,
|
||||
amount_minor BIGINT NOT NULL CHECK (amount_minor >= 0),
|
||||
currency CHAR(3) NOT NULL,
|
||||
status invoice_status NOT NULL DEFAULT 'requested',
|
||||
issued_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- at most one open (requested/issued) invoice per order
|
||||
CREATE UNIQUE INDEX one_open_invoice_per_order ON invoices (order_id)
|
||||
WHERE status IN ('requested', 'issued');
|
||||
|
||||
CREATE INDEX idx_invoices_user ON invoices (user_id);
|
||||
Reference in New Issue
Block a user