fix(finance): 历史订单退款不再进入新财务口径
- 断代前 archived 订单的退款只保留业务记录,不再生成冲减事实/总账 - 退款后的事实与费用重算只针对非归档订单 - 移除 refund_adjustment 渠道值的前端筛选与标签 - 迁移 116 的列约束保留,不再写入该值
This commit is contained in:
@@ -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' | ''
|
||||
|
||||
Reference in New Issue
Block a user