fix(finance): 历史订单退款不再进入新财务口径

- 断代前 archived 订单的退款只保留业务记录,不再生成冲减事实/总账
- 退款后的事实与费用重算只针对非归档订单
- 移除 refund_adjustment 渠道值的前端筛选与标签
- 迁移 116 的列约束保留,不再写入该值
This commit is contained in:
yml2213
2026-11-01 16:22:49 +08:00
parent a692723c73
commit 5d7904f6ee
6 changed files with 30 additions and 62 deletions
@@ -67,7 +67,6 @@ export type FulfillmentChannel =
| 'pending'
| 'archived'
| 'unclassified'
| 'refund_adjustment'
export const FULFILLMENT_CHANNELS = new Set<string>([
'work_order',
@@ -77,7 +76,6 @@ export const FULFILLMENT_CHANNELS = new Set<string>([
'pending',
'archived',
'unclassified',
'refund_adjustment',
])
/**
@@ -329,11 +329,11 @@ export async function createManualRefund(
})
// 比例类费用按净收入计:退款后重算相关订单项的费用快照,再回填事实。
const refundedItems = await query<{ order_item_id: number }>(
'SELECT DISTINCT order_item_id FROM order_refunds WHERE id = ANY($1::bigint[])',
[written.insertedIds],
)
try {
const refundedItems = await query<{ order_item_id: number }>(
'SELECT DISTINCT order_item_id FROM order_refunds WHERE id = ANY($1::bigint[])',
[written.insertedIds],
)
for (const row of refundedItems.rows) {
await syncChannelCostSnapshots({
afterId: Math.max(0, Number(row.order_item_id) - 1),
@@ -345,8 +345,10 @@ export async function createManualRefund(
// 快照重算失败不影响退款本身,成本快照管道会兜底。
}
const basisDate = toChinaDate(written.basisAt)
await refreshRefundDates(basisDate)
await refreshActiveRefundFacts(
refundedItems.rows.map((row) => Number(row.order_item_id)),
toChinaDate(written.basisAt),
)
const refunds = await listOrderRefunds(orderId)
return {
@@ -458,7 +460,14 @@ export async function voidOrderRefund(
})
const rebuiltDate = toChinaDate(context.basisAt)
await refreshRefundDates(rebuiltDate)
const voidedItem = await query<{ order_item_id: number }>(
'SELECT order_item_id FROM order_refunds WHERE id = $1',
[id],
)
await refreshActiveRefundFacts(
voidedItem.rows.map((row) => Number(row.order_item_id)),
rebuiltDate,
)
const refunds = await listOrderRefunds(context.orderId)
const refund = refunds.find((item) => item.id === id)
if (!refund) {
@@ -574,17 +583,20 @@ function toChinaDate(value: string): string {
}
/**
* 退款后回填事实与总账:
* - 原销售日:更新该订单项的收入/费用;
* - 退款日:生成历史订单(archived)的退款冲减行。
* 退款后回填事实与总账:只处理断代后的订单项。
* 历史归档订单的退款不进入新财务口径(业务侧自行处理)。
*/
async function refreshRefundDates(basisDate: string): Promise<void> {
const refundDate = CHINA_DATE_FORMATTER.format(new Date())
const dates = [basisDate, refundDate].filter(Boolean)
for (const date of [...new Set(dates)]) {
await rebuildChannelSalesDailyFacts({ dateFrom: date, dateTo: date })
await syncFinanceLedgerFromFacts({ dateFrom: date, dateTo: date })
}
async function refreshActiveRefundFacts(itemIds: number[], basisDate: string): Promise<void> {
const ids = [...new Set(itemIds.filter((id) => Number.isInteger(id) && id > 0))]
if (ids.length === 0 || !basisDate) return
const active = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM order_items
WHERE id = ANY($1::bigint[]) AND fulfillment_status <> 'archived'`,
[ids],
)
if (Number(active.rows[0]?.total || 0) === 0) return
await rebuildChannelSalesDailyFacts({ dateFrom: basisDate, dateTo: basisDate })
await syncFinanceLedgerFromFacts({ dateFrom: basisDate, dateTo: basisDate })
}
function formatFen(value: number): string {
@@ -169,47 +169,9 @@ export async function rebuildChannelSalesDailyFacts(input: {
[dateFrom, dateTo, computedAt],
)
// 断代后历史订单的退款:按退款发生日记冲减行,否则这类退款会彻底丢失。
const adjustments = await client.query(
`
WITH adj AS (
SELECT
(r.refunded_at AT TIME ZONE 'Asia/Shanghai')::date AS stat_date,
o.provider,
o.shop_id,
COUNT(DISTINCT o.id)::int AS order_count,
COUNT(*)::int AS item_count,
SUM(r.amount_fen)::bigint AS refund_fen
FROM order_refunds r
JOIN order_items oi ON oi.id = r.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE r.status = 'confirmed'
AND oi.fulfillment_status = 'archived'
AND (r.refunded_at AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
GROUP BY 1, 2, 3
)
INSERT INTO sales_daily_facts (
stat_date, provider, shop_id, canonical_product_id, channel,
order_count, item_count, quantity,
gross_revenue_fen, refund_amount_fen, net_revenue_fen,
cost_amount_fen, cost_missing_count, cost_stage,
gross_profit_fen, computed_at, created_at, updated_at
)
SELECT
stat_date, provider, shop_id, NULL, 'refund_adjustment',
order_count, item_count, 0,
0, refund_fen, -refund_fen,
0, 0, 'actual',
-refund_fen, $3::timestamptz, $3::timestamptz, $3::timestamptz
FROM adj
`,
[dateFrom, dateTo, computedAt],
)
return {
deleted: deleted.rowCount || 0,
inserted: (inserted.rowCount || 0) + (adjustments.rowCount || 0),
inserted: inserted.rowCount || 0,
skipped,
}
})
@@ -41,7 +41,6 @@ const FULFILLMENT_MODE_OPTIONS = [
{ value: 'third_party_voucher', label: '第三方卡券' },
{ value: 'manual', label: '人工客服' },
{ value: 'unclassified', label: '未分类' },
{ value: 'refund_adjustment', label: '历史退款冲减' },
{ value: 'archived', label: '历史归档' },
]
@@ -53,7 +52,6 @@ const FULFILLMENT_MODE_LABEL: Record<string, string> = {
manual: '人工客服',
archived: '历史归档',
unclassified: '未分类',
refund_adjustment: '历史退款冲减',
}
const COST_STAGE_LABEL: Record<string, { label: string; color: string }> = {
@@ -52,7 +52,6 @@ const FULFILLMENT_MODE_LABELS: Record<string, string> = {
pending: '待分类',
archived: '历史归档',
unclassified: '未分类',
refund_adjustment: '历史退款冲减',
}
const REASON_LABELS: Record<string, string> = {
@@ -8,7 +8,6 @@ export type FulfillmentChannel =
| 'third_party_voucher'
| 'legacy_platform'
| 'manual'
| 'refund_adjustment'
/** 商品来源:由 canonical_sku 前缀推导,空串表示未分类。 */
export type ProductSource = 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' | ''