Files
order_site/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql
T

402 lines
22 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 通用成本中心 + 标准商品 + 销售事实(建表草案)
-- ----------------------------------------------------------------------------
-- 状态:部分落地。商品主数据 + 渠道映射 + 成本中心已接入 `103_product_catalog.sql`、
-- `104_cost_center.sql`、`105_cost_center_bootstrap_fix.sql`;本文件仅保留设计追溯。
-- 尚未实施的 `sales_daily_facts` 等后续内容,接入时从新迁移号继续,并先跑 `npm run check:sql`。
-- 基线:main @ 7542d547;数据依据:本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14)。
-- 金额单位:分(BIGINT)。本文件所有 *_fen 列均为分。
-- 设计依据:docs/成本中心与三块履约模型.md(三块载体、成本版本与快照)、docs/财务系统集成方案.md。
-- 当前状态:商品主数据、渠道映射、成本中心已实施 P0;销售事实未实施。客服备用金支出已实施,但不是本文件的成本中心。
--
-- 目标(业务原话):以后改成本只在一个地方。
-- -> 所有成本金额只写 cost_versions;业务表只保存 cost_snapshot_id 与派生金额,永不回写历史。
-- ============================================================================
-- ============================================================================
-- §1 标准商品主数据
-- ----------------------------------------------------------------------------
-- 为什么必须先做:同一个商品名对应多个 sku_code(实测「苏尔南钻石100颗」47 个码、
-- 「指挥官密钥1个」42 个码),直接按 sku_code 或 sku_name 统计都会出错。
-- ============================================================================
CREATE TABLE IF NOT EXISTS product_catalog (
id BIGSERIAL PRIMARY KEY,
canonical_sku TEXT NOT NULL,
canonical_name TEXT NOT NULL,
category_id BIGINT,
unit_name TEXT NOT NULL DEFAULT '件',
status TEXT NOT NULL DEFAULT 'active',
attributes_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_catalog_canonical_sku ON product_catalog (canonical_sku);
CREATE INDEX IF NOT EXISTS idx_product_catalog_category ON product_catalog (category_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_status') THEN
ALTER TABLE product_catalog
ADD CONSTRAINT chk_product_catalog_status CHECK (status IN ('active', 'archived'));
END IF;
END $$;
COMMENT ON TABLE product_catalog IS '标准商品/皮肤主数据:报表统计口径的唯一入口';
COMMENT ON COLUMN product_catalog.canonical_sku IS '标准编码,报表维度用它,不用原始 sku_code';
COMMENT ON COLUMN product_catalog.unit_name IS '计量单位:件/个/颗,配合 normalized_quantity 使用';
-- 渠道映射:来源 + 店铺 + 平台SKU -> 标准商品
CREATE TABLE IF NOT EXISTS channel_product_mappings (
id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL,
shop_id TEXT NOT NULL DEFAULT '',
platform_sku_code TEXT NOT NULL,
platform_sku_name TEXT NOT NULL DEFAULT '',
canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
quantity_multiplier INTEGER NOT NULL DEFAULT 1,
confidence TEXT NOT NULL DEFAULT 'manual',
note TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
-- 同一来源+店铺+SKU 只能映射到一个标准商品;shop_id 为空表示该来源的全局映射
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_product_mappings_key
ON channel_product_mappings (provider, shop_id, platform_sku_code);
CREATE INDEX IF NOT EXISTS idx_channel_product_mappings_product
ON channel_product_mappings (canonical_product_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_confidence') THEN
ALTER TABLE channel_product_mappings
ADD CONSTRAINT chk_channel_product_mappings_confidence
CHECK (confidence IN ('exact', 'rule', 'manual'));
END IF;
END $$;
COMMENT ON TABLE channel_product_mappings IS '来源+店铺+平台SKU -> 标准商品映射(报表口径归一化)';
COMMENT ON COLUMN channel_product_mappings.quantity_multiplier IS 'SKU 名称内含数量时的换算系数,例如「5个」=5';
-- ============================================================================
-- §2 成本中心
-- ============================================================================
-- 成本项:定义成本类型与计价方式,不存金额
CREATE TABLE IF NOT EXISTS cost_items (
id BIGSERIAL PRIMARY KEY,
code TEXT NOT NULL,
name TEXT NOT NULL,
cost_type TEXT NOT NULL,
carrier_type TEXT NOT NULL,
pricing_mode TEXT NOT NULL,
currency TEXT NOT NULL DEFAULT 'CNY',
status TEXT NOT NULL DEFAULT 'active',
note TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_items_code ON cost_items (code);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_cost_type') THEN
ALTER TABLE cost_items
ADD CONSTRAINT chk_cost_items_cost_type
CHECK (cost_type IN ('auto_purchase', 'work_order_reward', 'manual_service', 'platform_fee', 'payment_fee', 'other'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_carrier_type') THEN
ALTER TABLE cost_items
ADD CONSTRAINT chk_cost_items_carrier_type
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual', 'shared'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_pricing_mode') THEN
ALTER TABLE cost_items
ADD CONSTRAINT chk_cost_items_pricing_mode
CHECK (pricing_mode IN ('per_item', 'per_order', 'per_hour', 'percent_of_revenue', 'fixed_daily'));
END IF;
END $$;
COMMENT ON TABLE cost_items IS '成本项定义:类型 + 载体 + 计价方式;金额一律放 cost_versions';
COMMENT ON COLUMN cost_items.carrier_type IS '适用执行载体;shared 表示与载体无关的通用成本(如支付通道费)';
-- 成本版本:**唯一改价入口**,按生效时间新增版本,永不覆盖历史
CREATE TABLE IF NOT EXISTS cost_versions (
id BIGSERIAL PRIMARY KEY,
cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
provider TEXT NOT NULL DEFAULT '',
shop_id TEXT NOT NULL DEFAULT '',
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
category_id BIGINT,
amount_fen BIGINT NOT NULL,
effective_from TIMESTAMPTZ NOT NULL,
effective_to TIMESTAMPTZ,
approval_status TEXT NOT NULL DEFAULT 'approved',
approved_by TEXT NOT NULL DEFAULT '',
reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
-- 同一成本项 + 同一适用范围 + 同一生效时间只能有一个版本
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_versions_scope
ON cost_versions (cost_item_id, provider, shop_id, coalesce(canonical_product_id, 0), effective_from);
CREATE INDEX IF NOT EXISTS idx_cost_versions_lookup
ON cost_versions (cost_item_id, effective_from DESC);
CREATE INDEX IF NOT EXISTS idx_cost_versions_product
ON cost_versions (canonical_product_id, effective_from DESC);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_amount') THEN
ALTER TABLE cost_versions ADD CONSTRAINT chk_cost_versions_amount CHECK (amount_fen >= 0);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_period') THEN
ALTER TABLE cost_versions
ADD CONSTRAINT chk_cost_versions_period
CHECK (effective_to IS NULL OR effective_to > effective_from);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_approval') THEN
ALTER TABLE cost_versions
ADD CONSTRAINT chk_cost_versions_approval
CHECK (approval_status IN ('draft', 'pending', 'approved', 'rejected'));
END IF;
END $$;
COMMENT ON TABLE cost_versions IS '成本版本:改价只在这里新增版本;历史成本靠 cost_snapshots 固化,不回写';
COMMENT ON COLUMN cost_versions.amount_fen IS '金额(分)。pricing_mode=percent_of_revenue 时表示万分比';
COMMENT ON COLUMN cost_versions.shop_id IS '空串=不限店铺;解析优先级 店铺+SKU > SKU > 分类 > 来源 > 全局';
-- 成本快照:每笔订单项/工单/人工单实际用了哪个版本、多少钱
CREATE TABLE IF NOT EXISTS cost_snapshots (
id BIGSERIAL PRIMARY KEY,
subject_type TEXT NOT NULL,
subject_id BIGINT NOT NULL,
order_id BIGINT,
order_item_id BIGINT,
work_order_id BIGINT,
carrier_type TEXT NOT NULL,
cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
cost_version_id BIGINT REFERENCES cost_versions(id) ON DELETE RESTRICT,
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
quantity NUMERIC(14, 3) NOT NULL DEFAULT 1,
unit_amount_fen BIGINT NOT NULL DEFAULT 0,
total_amount_fen BIGINT NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'resolved',
resolved_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_subject ON cost_snapshots (subject_type, subject_id);
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_item ON cost_snapshots (order_item_id);
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_work_order ON cost_snapshots (work_order_id);
-- 幂等:同一主体 + 同一成本项只允许一条快照
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_subject_item
ON cost_snapshots (subject_type, subject_id, cost_item_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_subject_type') THEN
ALTER TABLE cost_snapshots
ADD CONSTRAINT chk_cost_snapshots_subject_type
CHECK (subject_type IN ('order_item', 'work_order', 'manual_case'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_source') THEN
ALTER TABLE cost_snapshots
ADD CONSTRAINT chk_cost_snapshots_source
CHECK (source IN ('resolved', 'legacy_rule', 'manual_input', 'missing'));
END IF;
END $$;
COMMENT ON TABLE cost_snapshots IS '成本快照:下单/建工单/人工完成时固化,后续改价不影响历史';
COMMENT ON COLUMN cost_snapshots.source IS 'resolved=命中成本中心;legacy_rule=回退旧 work_product_rules 金额;missing=未匹配到(禁止默认 0)';
-- 成本异常队列:匹配不到成本不能默认 0,必须进队列
CREATE TABLE IF NOT EXISTS cost_exceptions (
id BIGSERIAL PRIMARY KEY,
subject_type TEXT NOT NULL,
subject_id BIGINT NOT NULL,
order_item_id BIGINT,
work_order_id BIGINT,
carrier_type TEXT NOT NULL,
reason TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
detail_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
resolved_at TIMESTAMPTZ,
resolved_by TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_exceptions_subject
ON cost_exceptions (subject_type, subject_id, reason);
CREATE INDEX IF NOT EXISTS idx_cost_exceptions_open
ON cost_exceptions (status, created_at DESC);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_exceptions_status') THEN
ALTER TABLE cost_exceptions
ADD CONSTRAINT chk_cost_exceptions_status CHECK (status IN ('open', 'resolved', 'ignored'));
END IF;
END $$;
COMMENT ON TABLE cost_exceptions IS '成本缺失/冲突异常队列:报表必须把 open 数量暴露出来,禁止静默按 0 计';
-- ============================================================================
-- §3 订单项口径补齐(金额与数量)
-- ----------------------------------------------------------------------------
-- 现状实测:order_items 只有 sku_code/sku_name/quantity/spec_json/snapshot,**没有任何金额列**;
-- 且 quantity 不可信(316,585 项里 316,578 项 = 1,名称带「N个」的有 104,710 项)。
-- 现状利好:目前每笔已支付订单恰好 1 个订单项(316,585 单 = 316,585 项),
-- 所以 orders.total_amount 暂时可当订单项收入;但 schema 允许多项,属**潜在**缺口。
--
-- 【口径已确认 2026-10-17】收入 = 用户付款金额,取 orders.total_amount:
-- · 91:total_amount 即用户付款(与快手发码配合)。数据核验:91 已支付 312,569 笔中
-- 235,271 笔含快手 kuaishouSendCode.paymentFen,其中 235,268 笔与 total_amount **完全一致**
-- (100.00%),仅 3 笔差 ¥1.80~2.00(2026-09-05 / 09-18 / 10-04,可单独抽查)。
-- · PDD:order_amount;抖音:pay_amount。三者同为“用户实付”,故统一 revenue_basis = 'user_payment'。
-- · 打手报酬 = **支出/成本**(最终由打手提现、我方付款),因此按**总额法**:
-- 收入记用户实付全额,打手报酬作为履约成本,不做代收代付净额。
-- · 退款/仲裁**暂未接入**:refunded_amount_fen / refunded_at 为**预留字段**,本轮不写逻辑。
-- ============================================================================
ALTER TABLE order_items
ADD COLUMN IF NOT EXISTS canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
ADD COLUMN IF NOT EXISTS normalized_quantity INTEGER,
ADD COLUMN IF NOT EXISTS revenue_amount_fen BIGINT,
ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS refunded_amount_fen BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS refunded_at TIMESTAMPTZ;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_basis') THEN
ALTER TABLE order_items
ADD CONSTRAINT chk_order_items_revenue_basis
CHECK (revenue_basis IN ('', 'user_payment', 'manual_adjustment'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_amount') THEN
ALTER TABLE order_items ADD CONSTRAINT chk_order_items_revenue_amount CHECK (revenue_amount_fen IS NULL OR revenue_amount_fen >= 0);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product ON order_items (canonical_product_id);
COMMENT ON COLUMN order_items.normalized_quantity IS '归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量';
COMMENT ON COLUMN order_items.revenue_amount_fen IS '收入(分)= 用户实付,取 orders.total_amount;revenue_basis 通常为 user_payment';
COMMENT ON COLUMN order_items.refunded_at IS '【预留】退款时间:退款/仲裁暂未接入,本轮不写逻辑;接入后按日净销售额 = 销售额 - 退款额';
-- 过渡:让工单规则逐步改用成本中心(有 cost_item_id 走成本中心,否则回退旧金额字段)
ALTER TABLE work_product_rules
ADD COLUMN IF NOT EXISTS cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
ADD COLUMN IF NOT EXISTS legacy_cost_source TEXT NOT NULL DEFAULT 'rule_amount';
CREATE INDEX IF NOT EXISTS idx_work_product_rules_cost_item ON work_product_rules (cost_item_id);
COMMENT ON COLUMN work_product_rules.cost_item_id IS '迁移用过渡列:非空则金额取成本中心,空则回退 reward_amount/unit_price_fen';
COMMENT ON COLUMN work_product_rules.legacy_cost_source IS '回退来源标记:rule_amount=旧规则金额,便于统计未迁移规则数';
-- 口径已确认(2026-10-17):打手报酬是**支出**(工单结算 -> 可提现 -> 提现打款),
-- 按总额法计入履约成本,与用户实付收入分开记,不做代收代付净额。
-- ============================================================================
-- §4 销售事实表(店铺 × 日期 × 标准商品)
-- ============================================================================
CREATE TABLE IF NOT EXISTS sales_daily_facts (
id BIGSERIAL PRIMARY KEY,
stat_date DATE NOT NULL,
provider TEXT NOT NULL,
shop_id TEXT NOT NULL DEFAULT '',
canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
carrier_type TEXT NOT NULL,
order_count INTEGER NOT NULL DEFAULT 0,
item_count INTEGER NOT NULL DEFAULT 0,
gross_revenue_fen BIGINT NOT NULL DEFAULT 0,
refund_amount_fen BIGINT NOT NULL DEFAULT 0,
net_revenue_fen BIGINT NOT NULL DEFAULT 0,
cost_amount_fen BIGINT NOT NULL DEFAULT 0,
cost_missing_count INTEGER NOT NULL DEFAULT 0,
gross_profit_fen BIGINT NOT NULL DEFAULT 0,
computed_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_daily_facts_key
ON sales_daily_facts (stat_date, provider, shop_id, canonical_product_id, carrier_type);
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_date ON sales_daily_facts (stat_date DESC);
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_shop ON sales_daily_facts (provider, shop_id, stat_date DESC);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_carrier') THEN
ALTER TABLE sales_daily_facts
ADD CONSTRAINT chk_sales_daily_facts_carrier
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_profit') THEN
ALTER TABLE sales_daily_facts
ADD CONSTRAINT chk_sales_daily_facts_profit
CHECK (gross_profit_fen = net_revenue_fen - cost_amount_fen);
END IF;
END $$;
COMMENT ON TABLE sales_daily_facts IS '店铺×日期(Asia/Shanghai)×标准商品×载体 的销售与成本事实;报表直接用这张表';
COMMENT ON COLUMN sales_daily_facts.stat_date IS '销售日按 Asia/Shanghai 归属,取 paid_at;退款按 refunded_at 归期';
COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS '成本缺失件数:>0 时该行毛利不可信,报表必须标注';
-- ============================================================================
-- §5 校验与重算(跑完后必须对平,差异不为 0 说明实现有问题)
-- ============================================================================
-- Q1 成本解析覆盖率:open 异常占比应低于约定阈值,且不得为 0 成本静默通过
-- SELECT carrier_type, count(*) FILTER (WHERE source = 'missing') AS missing,
-- count(*) AS total,
-- round(100.0 * count(*) FILTER (WHERE source = 'missing') / count(*), 2) AS missing_pct
-- FROM cost_snapshots GROUP BY 1;
-- Q2 毛利守恒:销售事实的净收入必须等于订单项收入汇总(按同一维度)
-- SELECT f.stat_date, f.provider, f.shop_id,
-- f.net_revenue_fen AS fact_revenue,
-- coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0) AS item_revenue
-- FROM sales_daily_facts f
-- LEFT JOIN order_items oi ON oi.canonical_product_id = f.canonical_product_id
-- LEFT JOIN orders o ON o.id = oi.order_id AND o.provider = f.provider AND o.shop_id = f.shop_id
-- GROUP BY 1, 2, 3, f.net_revenue_fen
-- HAVING f.net_revenue_fen <> coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0)
-- LIMIT 100;
-- Q3 历史成本不回写校验:改 cost_versions 后,已有 cost_snapshots 金额不得变化
-- SELECT count(*) FROM cost_snapshots WHERE resolved_at < now() - interval '1 day' AND created_at <> resolved_at;
-- ============================================================================
-- §6 落地顺序(与 docs/成本中心与三块履约模型.md §九 一致)
-- 1) §1 商品主数据 + 渠道映射 -> 解决「同一商品 47 个 SKU 码」与销量口径
-- 2) §3 订单项口径(数量/收入)-> 解决 7.3 倍数量低估与收入口径不统一
-- 3) §2 成本中心影子期 -> 自动发货接上游固定成本;工单接现有规则金额;人工接按单成本
-- 4) §4 销售事实表 -> 店铺/日期/皮肤 的销量、收入、成本、毛利
-- 5) 之后才是影子总账(docs/财务系统集成方案.md)
--
-- 【口径已确认 2026-10-17】
-- a) 收入 = 用户付款金额(orders.total_amount),计收入;已用数据核验与快手 paymentFen 100% 对齐。
-- b) 打手报酬 = 支出/成本(最终提现打款),按总额法。
-- c) 人工块:第一优先级是**人工支出可追溯**(付款项目/金额/订单号/付款人/凭证),
-- 见 `2026-10-17-人工支出登记-DDL草案.sql`;按单还是按小时的取值口径仍待定,先按单登记。
-- d) 退款/仲裁:**暂未接入,先预留**(字段 + reversal_of_id + 事件类型,不实现逻辑)。
--
-- 【仍待确认】
-- 1) 收入确认时点(付款 / 发货 / 验收 / 售后期结束)——影响利润表与预收账款;
-- 2) 自动发货 cost_amount 是否稳定、是否含平台积分/汇率;
-- 3) 平台服务费/支付通道费由谁承担、结算周期与账单口径;
-- 4) 寄卖货款的采购成本确认时点。
-- ============================================================================