feat(finance): 完成成本快照运行层、销售事实与人工支出登记
迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
@@ -200,6 +200,7 @@ type OrderItemCostContext = {
|
||||
normalizedQuantity: number
|
||||
canonicalProductId: number | null
|
||||
revenueFen: number
|
||||
basisAt: string
|
||||
workOrderId: number | null
|
||||
workProductRuleId: number | null
|
||||
hasAffiliateDashTask: boolean
|
||||
@@ -2632,6 +2633,8 @@ export async function previewAdminCost(payload: JsonObject = {}) {
|
||||
const mapping = await findMappingForContext(context)
|
||||
const productId = context.canonicalProductId || mapping?.canonical_product_id || null
|
||||
const product = productId ? mapProduct(await getProductRow(productId)) : null
|
||||
const asOf =
|
||||
normalizeIsoDate(payload.asOf ?? payload.costBasisAt, '成本基准时间', true) || context.basisAt
|
||||
const resolution: CostResolution = productId
|
||||
? await resolveCostLines({
|
||||
provider: context.provider,
|
||||
@@ -2641,6 +2644,7 @@ export async function previewAdminCost(payload: JsonObject = {}) {
|
||||
workProductRuleId: context.workProductRuleId,
|
||||
quantity: context.normalizedQuantity,
|
||||
revenueFen: context.revenueFen,
|
||||
asOf,
|
||||
})
|
||||
: { lines: [], missingCostItems: [], applicableCostItemCount: 0 }
|
||||
|
||||
@@ -2677,6 +2681,7 @@ export async function previewAdminCost(payload: JsonObject = {}) {
|
||||
return {
|
||||
context: {
|
||||
...context,
|
||||
basisAt: asOf,
|
||||
carrierType,
|
||||
},
|
||||
mapping: mapping ? mapMapping(mapping) : null,
|
||||
@@ -2698,23 +2703,61 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const stage = payload.snapshotStage === 'actual' ? 'actual' : 'estimated'
|
||||
const snapshots: Array<Record<string, unknown>> = []
|
||||
let exceptionCreatedCount = 0
|
||||
let supersededCount = 0
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
for (const line of preview.lines) {
|
||||
const currentResult = await client.query<{
|
||||
id: number
|
||||
snapshot_stage: string
|
||||
total_amount_fen: number
|
||||
}>(
|
||||
`
|
||||
SELECT id, snapshot_stage, total_amount_fen
|
||||
FROM cost_snapshots
|
||||
WHERE subject_type = 'order_item'
|
||||
AND subject_id = $1
|
||||
AND cost_item_id = $2
|
||||
AND superseded_at IS NULL
|
||||
FOR UPDATE
|
||||
`,
|
||||
[orderItemId, line.costItemId],
|
||||
)
|
||||
const current = currentResult.rows[0]
|
||||
if (current) {
|
||||
// 预估快照一旦生成就代表支付时点的成本,不因后续改价重写;
|
||||
// 只有实际成本允许替代当前有效行。
|
||||
if (stage === 'estimated') continue
|
||||
if (
|
||||
current.snapshot_stage === 'actual' &&
|
||||
Number(current.total_amount_fen) === line.totalAmountFen
|
||||
) {
|
||||
continue
|
||||
}
|
||||
await client.query(
|
||||
`UPDATE cost_snapshots SET superseded_at = $1 WHERE id = $2 AND superseded_at IS NULL`,
|
||||
[now, current.id],
|
||||
)
|
||||
supersededCount += 1
|
||||
}
|
||||
|
||||
const result = await client.query<{ [column: string]: unknown }>(
|
||||
`
|
||||
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, resolved_at, created_at
|
||||
metadata_json, cost_basis_at, resolved_at, created_at
|
||||
) VALUES (
|
||||
'order_item', $1, $2, $1, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, 'resolved', $12::jsonb, $13, $13
|
||||
$8, $9, $10, $11, 'resolved', $12::jsonb, $13, $14, $14
|
||||
)
|
||||
ON CONFLICT (subject_type, subject_id, cost_item_id) DO NOTHING
|
||||
ON CONFLICT (subject_type, subject_id, cost_item_id)
|
||||
WHERE superseded_at IS NULL
|
||||
DO NOTHING
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
@@ -2728,14 +2771,16 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
|
||||
line.quantity,
|
||||
line.unitAmountFen,
|
||||
line.totalAmountFen,
|
||||
payload.snapshotStage === 'actual' ? 'actual' : 'estimated',
|
||||
stage,
|
||||
JSON.stringify({
|
||||
skuCode: preview.context.skuCode,
|
||||
skuName: preview.context.skuName,
|
||||
provider: preview.context.provider,
|
||||
shopId: preview.context.shopId,
|
||||
normalizedQuantity: preview.context.normalizedQuantity,
|
||||
basisAt: preview.context.basisAt,
|
||||
}),
|
||||
preview.context.basisAt,
|
||||
now,
|
||||
],
|
||||
)
|
||||
@@ -2768,7 +2813,13 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
|
||||
}
|
||||
})
|
||||
|
||||
return { preview, snapshots, createdCount: snapshots.length, exceptionCreatedCount }
|
||||
return {
|
||||
preview,
|
||||
snapshots,
|
||||
createdCount: snapshots.length,
|
||||
supersededCount,
|
||||
exceptionCreatedCount,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostContext> {
|
||||
@@ -2787,6 +2838,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
normalized_quantity: number | null
|
||||
canonical_product_id: number | null
|
||||
revenue_fen: number
|
||||
basis_at: string
|
||||
work_order_id: number | null
|
||||
work_product_rule_id: number | null
|
||||
has_affiliate_dash_task: boolean
|
||||
@@ -2804,7 +2856,8 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
oi.quantity,
|
||||
oi.normalized_quantity,
|
||||
oi.canonical_product_id,
|
||||
o.total_amount AS revenue_fen,
|
||||
oi.revenue_amount_fen AS revenue_fen,
|
||||
COALESCE(o.paid_at, o.created_at) AS basis_at,
|
||||
wo.id AS work_order_id,
|
||||
wo.product_rule_id AS work_product_rule_id,
|
||||
EXISTS (
|
||||
@@ -2813,7 +2866,13 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
) AS has_affiliate_dash_task
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN work_orders wo ON wo.order_item_id = oi.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 oi.id = $1
|
||||
LIMIT 1
|
||||
`,
|
||||
@@ -2844,6 +2903,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
: Math.max(1, resolveSkuNameQuantity(row.sku_name || row.sku_code)),
|
||||
canonicalProductId: row.canonical_product_id ? Number(row.canonical_product_id) : null,
|
||||
revenueFen: Math.max(0, Number(row.revenue_fen) || 0),
|
||||
basisAt: new Date(row.basis_at).toISOString(),
|
||||
workOrderId: row.work_order_id ? Number(row.work_order_id) : null,
|
||||
workProductRuleId: row.work_product_rule_id ? Number(row.work_product_rule_id) : null,
|
||||
hasAffiliateDashTask: Boolean(row.has_affiliate_dash_task),
|
||||
@@ -2869,6 +2929,8 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
payload.canonicalProductId ?? payload.canonical_product_id,
|
||||
),
|
||||
revenueFen: Math.max(0, Math.trunc(Number(payload.revenueFen ?? payload.revenue_fen) || 0)),
|
||||
basisAt:
|
||||
normalizeIsoDate(payload.asOf ?? payload.costBasisAt, '成本基准时间', true) || nowIso(),
|
||||
workOrderId: null,
|
||||
workProductRuleId: normalizeOptionalId(
|
||||
payload.workProductRuleId ?? payload.work_product_rule_id,
|
||||
@@ -2933,6 +2995,7 @@ async function resolveCostLines(input: {
|
||||
workProductRuleId: number | null
|
||||
quantity: number
|
||||
revenueFen: number
|
||||
asOf?: string
|
||||
}): Promise<CostResolution> {
|
||||
const result = await query<
|
||||
CostItemRow & {
|
||||
@@ -2981,7 +3044,7 @@ async function resolveCostLines(input: {
|
||||
ORDER BY ci.id
|
||||
`,
|
||||
[
|
||||
nowIso(),
|
||||
input.asOf || nowIso(),
|
||||
input.provider,
|
||||
input.shopId,
|
||||
input.canonicalProductId,
|
||||
|
||||
Reference in New Issue
Block a user