feat(finance): 完成成本快照运行层、销售事实与人工支出登记

迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
yml2213
2026-10-22 16:41:03 +08:00
parent 15989af87c
commit 0889235870
39 changed files with 4824 additions and 143 deletions
@@ -0,0 +1,580 @@
import type { PoolClient } from 'pg'
import { withTransaction } from '../../db/client.js'
import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
export type CostSnapshotSyncInput = {
dateFrom?: unknown
dateTo?: unknown
from?: unknown
to?: unknown
}
export type CostSnapshotSyncResult = {
range: { fromDate: string; toDate: string }
createdCount: number
exceptionCreatedCount: number
resolvedExceptionCount: number
}
export type ActualCostSnapshotSyncResult = {
range: { fromDate: string; toDate: string }
createdCount: number
upgradedCount: number
directRewardCount: number
}
/**
* 按订单项批量生成预估成本快照。
*
* 只处理已有 canonical_product_id 的订单项;未归一订单项进入
* product_mapping_missing 异常,不在这里猜测商品。
*/
export async function syncEstimatedOrderItemCostSnapshots(
input: CostSnapshotSyncInput = {},
): Promise<CostSnapshotSyncResult> {
const range = resolveFinanceDateRange(input, { defaultDays: 7, maxDays: 400 })
return withTransaction(async (client) => {
const inserted = await client.query(
`
WITH item_scope AS (
SELECT
oi.id AS order_item_id,
oi.order_id,
o.provider,
o.shop_id,
oi.sku_code,
oi.sku_name,
oi.canonical_product_id,
COALESCE(o.paid_at, o.created_at) AS basis_at,
GREATEST(COALESCE(oi.normalized_quantity, oi.quantity, 1), 1)::numeric AS quantity,
COALESCE(oi.revenue_amount_fen, 0)::bigint AS revenue_fen,
wo.id AS work_order_id,
wo.product_rule_id AS work_product_rule_id,
CASE
WHEN wo.id IS NOT NULL THEN 'work_order'
WHEN EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id
AND ft.executor_key = 'affiliate_dash'
) THEN 'affiliate_dash'
ELSE 'manual'
END AS carrier_type
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN LATERAL (
SELECT id, product_rule_id
FROM work_orders
WHERE order_item_id = oi.id
ORDER BY id DESC
LIMIT 1
) wo ON TRUE
WHERE o.pay_status IN ('paid', 'refunded')
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
),
resolved AS (
SELECT
s.*,
ci.id AS cost_item_id,
ci.pricing_mode,
v.id AS cost_version_id,
v.amount_fen,
v.rate_bps
FROM item_scope s
JOIN cost_items ci ON ci.status = 'active'
AND ci.carrier_type IN (s.carrier_type, 'shared')
AND ci.pricing_mode IN ('per_item', 'per_order', 'percent_of_revenue')
AND ci.code <> 'sys_work_order_reward'
JOIN LATERAL (
SELECT v.id, v.amount_fen, v.rate_bps
FROM cost_versions v
WHERE v.cost_item_id = ci.id
AND v.status = 'active'
AND v.effective_from <= s.basis_at
AND (v.effective_to IS NULL OR v.effective_to > s.basis_at)
AND (
(v.scope_type = 'rule' AND s.work_product_rule_id IS NOT NULL
AND v.work_product_rule_id = s.work_product_rule_id)
OR (v.scope_type = 'product' AND s.canonical_product_id IS NOT NULL
AND v.canonical_product_id = s.canonical_product_id)
OR (v.scope_type = 'shop' AND v.provider = s.provider AND v.shop_id = s.shop_id)
OR (v.scope_type = 'provider' AND v.provider = s.provider)
OR (v.scope_type = 'global')
)
ORDER BY
CASE v.scope_type
WHEN 'rule' THEN 1
WHEN 'product' THEN 2
WHEN 'shop' THEN 3
WHEN 'provider' THEN 4
ELSE 5
END,
v.effective_from DESC,
v.id DESC
LIMIT 1
) v ON TRUE
WHERE s.canonical_product_id IS NOT NULL
),
calculated AS (
SELECT
r.*,
CASE r.pricing_mode
WHEN 'per_item' THEN ROUND(r.amount_fen::numeric * r.quantity)::bigint
WHEN 'per_order' THEN r.amount_fen
WHEN 'percent_of_revenue'
THEN ROUND((r.revenue_fen::numeric * r.rate_bps::numeric) / 10000.0)::bigint
ELSE r.amount_fen
END AS total_amount_fen
FROM resolved r
)
INSERT INTO cost_snapshots (
subject_type, subject_id, order_id, order_item_id, work_order_id,
carrier_type, cost_item_id, cost_version_id, canonical_product_id,
quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source,
metadata_json, cost_basis_at, resolved_at, created_at
)
SELECT
'order_item', c.order_item_id, c.order_id, c.order_item_id, c.work_order_id,
c.carrier_type, c.cost_item_id, c.cost_version_id, c.canonical_product_id,
CASE WHEN c.pricing_mode = 'per_item' THEN c.quantity ELSE 1 END,
CASE WHEN c.pricing_mode = 'percent_of_revenue' THEN c.total_amount_fen ELSE c.amount_fen END,
c.total_amount_fen,
'estimated', 'resolved',
jsonb_build_object(
'provider', c.provider,
'shopId', c.shop_id,
'skuCode', c.sku_code,
'skuName', c.sku_name,
'pricingMode', c.pricing_mode,
'revenueFen', c.revenue_fen,
'normalizedQuantity', c.quantity,
'basisAt', c.basis_at
),
c.basis_at, NOW(), NOW()
FROM calculated c
ON CONFLICT (subject_type, subject_id, cost_item_id)
WHERE superseded_at IS NULL
DO NOTHING
`,
[range.fromDate, range.toDate],
)
const productExceptions = await insertAndResolveProductMappingExceptions(
client,
range.fromDate,
range.toDate,
)
const costExceptions = await insertAndResolveCostMissingExceptions(
client,
range.fromDate,
range.toDate,
)
return {
range,
createdCount: inserted.rowCount || 0,
exceptionCreatedCount: productExceptions.created + costExceptions.created,
resolvedExceptionCount: productExceptions.resolved + costExceptions.resolved,
}
})
}
/**
* 工单验收后,把订单项当前预估快照升级为实际快照。
*
* 如果配置了 work_order_reward 成本项,则用工单实际 reward_amount 覆盖金额;
* 否则仍冻结当前版本金额,但把阶段标为 actual。旧快照保留 superseded_at。
*/
export async function syncActualWorkOrderCostSnapshots(
input: CostSnapshotSyncInput = {},
): Promise<ActualCostSnapshotSyncResult> {
const range = resolveFinanceDateRange(input, { defaultDays: 30, maxDays: 400 })
return withTransaction(async (client) => {
const directReward = await syncDirectWorkOrderRewardSnapshots(
client,
range.fromDate,
range.toDate,
)
const upgraded = await client.query(
`
WITH accepted AS (
SELECT DISTINCT ON (wo.order_item_id)
wo.id AS work_order_id,
wo.order_item_id,
GREATEST(COALESCE(wo.reward_amount, 0), 0)::bigint AS reward_amount,
COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at, NOW()) AS accepted_at
FROM work_orders wo
JOIN order_items oi ON oi.id = wo.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE wo.status = 'accepted'
AND wo.order_item_id IS NOT NULL
AND (COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at)
AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $1::date AND $2::date
ORDER BY wo.order_item_id, wo.id DESC
),
active AS (
SELECT
cs.*,
a.work_order_id AS actual_work_order_id,
a.reward_amount,
a.accepted_at,
ci.cost_type
FROM cost_snapshots cs
JOIN accepted a ON cs.subject_type = 'order_item' AND cs.subject_id = a.order_item_id
JOIN cost_items ci ON ci.id = cs.cost_item_id
WHERE cs.superseded_at IS NULL
AND cs.snapshot_stage IN ('estimated', 'actual')
AND ci.code <> 'sys_work_order_reward'
),
target AS (
SELECT *
FROM active
WHERE snapshot_stage = 'estimated'
OR total_amount_fen <> CASE
WHEN cost_type = 'work_order_reward' AND reward_amount > 0 THEN reward_amount
ELSE total_amount_fen
END
),
superseded AS (
UPDATE cost_snapshots cs
SET superseded_at = NOW()
FROM target t
WHERE cs.id = t.id
AND cs.superseded_at IS NULL
RETURNING
cs.id, cs.subject_id, cs.order_id, cs.order_item_id, cs.carrier_type,
cs.cost_item_id, cs.cost_version_id, cs.canonical_product_id,
cs.quantity, cs.unit_amount_fen, cs.total_amount_fen, cs.metadata_json
)
INSERT INTO cost_snapshots (
subject_type, subject_id, order_id, order_item_id, work_order_id,
carrier_type, cost_item_id, cost_version_id, canonical_product_id,
quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source,
metadata_json, cost_basis_at, resolved_at, created_at
)
SELECT
'order_item', s.subject_id, s.order_id, s.order_item_id, a.work_order_id,
s.carrier_type, s.cost_item_id, s.cost_version_id, s.canonical_product_id,
CASE WHEN ci.cost_type = 'work_order_reward' AND a.reward_amount > 0 THEN 1
ELSE s.quantity END,
CASE WHEN ci.cost_type = 'work_order_reward' AND a.reward_amount > 0
THEN a.reward_amount ELSE s.unit_amount_fen END,
CASE WHEN ci.cost_type = 'work_order_reward' AND a.reward_amount > 0
THEN a.reward_amount ELSE s.total_amount_fen END,
'actual', 'resolved',
s.metadata_json || jsonb_build_object(
'actualWorkOrderId', a.work_order_id,
'actualRewardFen', a.reward_amount,
'basisAt', a.accepted_at
),
a.accepted_at, NOW(), NOW()
FROM superseded s
JOIN accepted a ON a.order_item_id = s.subject_id
JOIN cost_items ci ON ci.id = s.cost_item_id
ON CONFLICT (subject_type, subject_id, cost_item_id)
WHERE superseded_at IS NULL
DO NOTHING
`,
[range.fromDate, range.toDate],
)
return {
range,
createdCount: directReward.inserted + (upgraded.rowCount || 0),
upgradedCount: upgraded.rowCount || 0,
directRewardCount: directReward.inserted,
}
})
}
/**
* 接单平台的实际成本不依赖预估快照:工单验收后直接按 reward_amount 写 actual。
* 系统成本项 sys_work_order_reward 的版本金额只是占位,不会进入预估阶段。
*/
async function syncDirectWorkOrderRewardSnapshots(
client: PoolClient,
fromDate: string,
toDate: string,
) {
const superseded = await client.query(
`
WITH system_item AS (
SELECT ci.id AS cost_item_id, v.id AS cost_version_id
FROM cost_items ci
JOIN LATERAL (
SELECT id
FROM cost_versions
WHERE cost_item_id = ci.id
AND status = 'active'
AND effective_from <= NOW()
ORDER BY effective_from DESC, id DESC
LIMIT 1
) v ON TRUE
WHERE ci.code = 'sys_work_order_reward'
AND ci.status = 'active'
),
accepted AS (
SELECT DISTINCT ON (wo.order_item_id)
wo.id AS work_order_id,
wo.order_item_id,
GREATEST(COALESCE(wo.reward_amount, 0), 0)::bigint AS reward_amount,
COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at, NOW()) AS accepted_at
FROM work_orders wo
JOIN order_items oi ON oi.id = wo.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE wo.status = 'accepted'
AND wo.order_item_id IS NOT NULL
AND COALESCE(wo.reward_amount, 0) > 0
AND (COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at)
AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $1::date AND $2::date
ORDER BY wo.order_item_id, wo.id DESC
)
UPDATE cost_snapshots cs
SET superseded_at = NOW()
FROM accepted a
JOIN system_item si ON TRUE
JOIN LATERAL (
SELECT id, snapshot_stage, total_amount_fen
FROM cost_snapshots
WHERE subject_type = 'order_item'
AND subject_id = a.order_item_id
AND cost_item_id = si.cost_item_id
AND superseded_at IS NULL
ORDER BY id DESC
LIMIT 1
FOR UPDATE
) current ON TRUE
WHERE cs.id = current.id
AND cs.superseded_at IS NULL
AND (current.snapshot_stage <> 'actual' OR current.total_amount_fen <> a.reward_amount)
`,
[fromDate, toDate],
)
const inserted = await client.query(
`
WITH system_item AS (
SELECT ci.id AS cost_item_id, v.id AS cost_version_id
FROM cost_items ci
JOIN LATERAL (
SELECT id
FROM cost_versions
WHERE cost_item_id = ci.id
AND status = 'active'
AND effective_from <= NOW()
ORDER BY effective_from DESC, id DESC
LIMIT 1
) v ON TRUE
WHERE ci.code = 'sys_work_order_reward'
AND ci.status = 'active'
),
accepted AS (
SELECT DISTINCT ON (wo.order_item_id)
wo.id AS work_order_id,
wo.order_item_id,
GREATEST(COALESCE(wo.reward_amount, 0), 0)::bigint AS reward_amount,
COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at, NOW()) AS accepted_at
FROM work_orders wo
JOIN order_items oi ON oi.id = wo.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE wo.status = 'accepted'
AND wo.order_item_id IS NOT NULL
AND COALESCE(wo.reward_amount, 0) > 0
AND (COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at)
AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $1::date AND $2::date
ORDER BY wo.order_item_id, wo.id DESC
)
INSERT INTO cost_snapshots (
subject_type, subject_id, order_id, order_item_id, work_order_id,
carrier_type, cost_item_id, cost_version_id, canonical_product_id,
quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source,
metadata_json, cost_basis_at, resolved_at, created_at
)
SELECT
'order_item', a.order_item_id, oi.order_id, a.order_item_id, a.work_order_id,
'work_order', si.cost_item_id, si.cost_version_id, oi.canonical_product_id,
1, a.reward_amount, a.reward_amount, 'actual', 'resolved',
jsonb_build_object(
'source', 'system_work_order_reward',
'actualWorkOrderId', a.work_order_id,
'actualRewardFen', a.reward_amount,
'basisAt', a.accepted_at
),
a.accepted_at, NOW(), NOW()
FROM accepted a
CROSS JOIN system_item si
JOIN order_items oi ON oi.id = a.order_item_id
WHERE NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'order_item'
AND cs.subject_id = a.order_item_id
AND cs.cost_item_id = si.cost_item_id
AND cs.superseded_at IS NULL
AND cs.snapshot_stage = 'actual'
AND cs.total_amount_fen = a.reward_amount
)
ON CONFLICT (subject_type, subject_id, cost_item_id)
WHERE superseded_at IS NULL
DO NOTHING
`,
[fromDate, toDate],
)
return {
inserted: inserted.rowCount || 0,
superseded: superseded.rowCount || 0,
}
}
async function insertAndResolveProductMappingExceptions(
client: PoolClient,
fromDate: string,
toDate: string,
) {
const inserted = await client.query(
`
WITH item_scope AS (
SELECT
oi.id AS order_item_id,
o.provider,
o.shop_id,
oi.sku_code,
oi.sku_name,
oi.canonical_product_id,
COALESCE(o.paid_at, o.created_at) AS basis_at,
wo.id AS work_order_id,
CASE
WHEN wo.id IS NOT NULL THEN 'work_order'
WHEN EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
) THEN 'affiliate_dash'
ELSE 'manual'
END AS carrier_type
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN LATERAL (
SELECT id FROM work_orders WHERE order_item_id = oi.id ORDER BY id DESC LIMIT 1
) wo ON TRUE
WHERE o.pay_status IN ('paid', 'refunded')
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
)
INSERT INTO cost_exceptions (
subject_type, subject_id, order_item_id, work_order_id,
carrier_type, reason, status, detail_json, created_at
)
SELECT
'order_item', s.order_item_id, s.order_item_id, s.work_order_id,
s.carrier_type, 'product_mapping_missing', 'open',
jsonb_build_object(
'provider', s.provider,
'shopId', s.shop_id,
'skuCode', s.sku_code,
'skuName', s.sku_name,
'basisAt', s.basis_at,
'message', '订单项没有归一到标准商品,销售事实会进入未归一桶'
),
NOW()
FROM item_scope s
WHERE s.canonical_product_id IS NULL
ON CONFLICT (subject_type, subject_id, reason) DO NOTHING
`,
[fromDate, toDate],
)
const resolved = await client.query(
`
UPDATE cost_exceptions ce
SET status = 'resolved', resolved_at = NOW()
WHERE ce.status = 'open'
AND ce.subject_type = 'order_item'
AND ce.reason = 'product_mapping_missing'
AND EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.id = ce.subject_id AND oi.canonical_product_id IS NOT NULL
)
`,
)
return { created: inserted.rowCount || 0, resolved: resolved.rowCount || 0 }
}
async function insertAndResolveCostMissingExceptions(
client: PoolClient,
fromDate: string,
toDate: string,
) {
const inserted = await client.query(
`
WITH item_scope AS (
SELECT
oi.id AS order_item_id,
oi.canonical_product_id,
COALESCE(o.paid_at, o.created_at) AS basis_at,
wo.id AS work_order_id,
CASE
WHEN wo.id IS NOT NULL THEN 'work_order'
WHEN EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
) THEN 'affiliate_dash'
ELSE 'manual'
END AS carrier_type
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN LATERAL (
SELECT id FROM work_orders WHERE order_item_id = oi.id ORDER BY id DESC LIMIT 1
) wo ON TRUE
WHERE o.pay_status IN ('paid', 'refunded')
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
)
INSERT INTO cost_exceptions (
subject_type, subject_id, order_item_id, work_order_id,
carrier_type, reason, status, detail_json, created_at
)
SELECT
'order_item', s.order_item_id, s.order_item_id, s.work_order_id,
s.carrier_type, 'cost_missing', 'open',
jsonb_build_object(
'canonicalProductId', s.canonical_product_id,
'carrierType', s.carrier_type,
'basisAt', s.basis_at,
'message', '订单项没有匹配到任何成本版本'
),
NOW()
FROM item_scope s
WHERE s.canonical_product_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'order_item'
AND cs.subject_id = s.order_item_id
AND cs.superseded_at IS NULL
)
ON CONFLICT (subject_type, subject_id, reason) DO NOTHING
`,
[fromDate, toDate],
)
const resolved = await client.query(
`
UPDATE cost_exceptions ce
SET status = 'resolved', resolved_at = NOW()
WHERE ce.status = 'open'
AND ce.subject_type = 'order_item'
AND ce.reason = 'cost_missing'
AND EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'order_item'
AND cs.subject_id = ce.subject_id
AND cs.superseded_at IS NULL
)
`,
)
return { created: inserted.rowCount || 0, resolved: resolved.rowCount || 0 }
}