From 8e3b8e0a323b3907278cf0994660046bed1451a0 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 31 Oct 2026 20:17:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(finance):=20=E9=94=80=E5=94=AE=E4=BA=8B?= =?UTF-8?q?=E5=AE=9E=E5=8F=AA=E7=BB=9F=E8=AE=A1=E5=B7=B2=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=B9=B6=E6=89=A3=E5=87=8F=E9=80=80=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 事实范围增加 pay_status IN ('paid','refunded') - 关联 order_refunds 已确认退款,净收入/毛利按扣除退款计算 - 补回归断言,防止再次出现退款不减收入 --- .../services/finance/finance-controls.test.ts | 19 +++++++++++++++++++ .../finance/sales-facts-channel-service.ts | 15 ++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/services/finance/finance-controls.test.ts b/apps/backend/src/services/finance/finance-controls.test.ts index 717a2815..5346d47e 100644 --- a/apps/backend/src/services/finance/finance-controls.test.ts +++ b/apps/backend/src/services/finance/finance-controls.test.ts @@ -152,3 +152,22 @@ test('销售毛利必须同时暴露缺失成本收入,避免只看毛利绝 assert.ok(source.includes('cost_missing_revenue_fen')) assert.ok(source.includes('costMissingRevenueFen')) }) + +test('销售事实必须只统计已支付订单,并扣减已确认退款', async () => { + const source = await read(new URL('./sales-facts-channel-service.ts', import.meta.url)) + assert.ok(source.includes("o.pay_status IN ('paid', 'refunded')"), '必须过滤未支付订单') + assert.ok(source.includes('order_refunds'), '必须读取退款表') + assert.ok(source.includes("r.status = 'confirmed'"), '只计入已确认退款') + assert.ok(source.includes('SUM(revenue_fen) - SUM(refund_fen)'), '净收入必须扣减退款') + assert.ok( + source.includes('SUM(revenue_fen) - SUM(refund_fen) - SUM(cost_fen)'), + '毛利必须基于净收入', + ) +}) + +test('人工确认后必须立刻重算该业务日事实与总账', async () => { + const source = await read(new URL('./manual-fulfillment-service.ts', import.meta.url)) + assert.ok(source.includes('refreshFactsForOrderItem'), '确认后必须刷新事实') + assert.ok(source.includes('rebuildChannelSalesDailyFacts'), '必须重算渠道销售事实') + assert.ok(source.includes('syncFinanceLedgerFromFacts'), '必须同步总账') +}) diff --git a/apps/backend/src/services/finance/sales-facts-channel-service.ts b/apps/backend/src/services/finance/sales-facts-channel-service.ts index 3954bb10..18abecff 100644 --- a/apps/backend/src/services/finance/sales-facts-channel-service.ts +++ b/apps/backend/src/services/finance/sales-facts-channel-service.ts @@ -77,16 +77,25 @@ export async function rebuildChannelSalesDailyFacts(input: { FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status IN ('assigned', 'pending') + AND 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 ), + refunded AS ( + SELECT r.order_item_id, SUM(r.amount_fen)::bigint AS refund_fen + FROM order_refunds r + WHERE r.status = 'confirmed' + GROUP BY r.order_item_id + ), costed AS ( SELECT s.*, + COALESCE(rf.refund_fen, 0)::bigint AS refund_fen, COALESCE(c.cost_fen, 0)::bigint AS cost_fen, (c.order_item_id IS NULL) AS cost_missing, COALESCE(c.has_actual, FALSE) AS has_actual FROM scope s + LEFT JOIN refunded rf ON rf.order_item_id = s.order_item_id LEFT JOIN ( SELECT cs.order_item_id, @@ -114,8 +123,8 @@ export async function rebuildChannelSalesDailyFacts(input: { COUNT(*)::int, SUM(quantity), SUM(revenue_fen), - 0, - SUM(revenue_fen), + SUM(refund_fen), + SUM(revenue_fen) - SUM(refund_fen), SUM(cost_fen), COUNT(*) FILTER (WHERE cost_missing)::int, CASE @@ -124,7 +133,7 @@ export async function rebuildChannelSalesDailyFacts(input: { WHEN BOOL_AND(has_actual) THEN 'actual' ELSE 'estimated' END, - SUM(revenue_fen) - SUM(cost_fen), + SUM(revenue_fen) - SUM(refund_fen) - SUM(cost_fen), $3::timestamptz, $3::timestamptz, $3::timestamptz