import process from 'node:process' import { closeDb, query } from '../src/db/client.js' import { assignPendingOrderItemChannels } from '../src/services/fulfillment/channel-assign-service.js' import { normalizeOrderItemProducts } from '../src/services/cost-center/product-normalize-service.js' import { syncChannelCostSnapshots } from '../src/services/cost-center/cost-snapshot-service.js' import { rebuildChannelSalesDailyFacts } from '../src/services/finance/sales-facts-channel-service.js' import { syncFinanceLedgerFromFacts } from '../src/services/finance/finance-ledger-service.js' import { formatShanghaiDate } from '../src/utils/finance-date-range.js' /** * 断代后重建:从 cutover 日起重跑三条管道,生成成本快照、销售事实与期初分录。 * * 幂等:渠道判定/商品归一/成本快照都按「缺什么补什么」推进,事实按日先删后插, * 总账按「event_type + 日期」幂等。可以反复执行。 * * 用法: * npm run finance:rebuild-cutover -- --date-from=2026-10-31 --date-to=2026-10-31 * npm run finance:rebuild-cutover -- --max-batches=200 */ const options = parseOptions(process.argv.slice(2)) const dateFrom = String(options.dateFrom || '2026-10-31').slice(0, 10) const dateTo = String(options.dateTo || formatShanghaiDate(new Date())).slice(0, 10) const maxBatches = clampInt(options.maxBatches, 200, 1, 2000) const batchSize = clampInt(options.batchSize, 2000, 1, 5000) async function main() { await assertSchema() const startedAt = Date.now() const channel = await assignPendingOrderItemChannels({ batchSize, maxBatches }) const product = await normalizeOrderItemProducts({ batchSize, maxBatches }) const cost = await syncChannelCostSnapshots({ batchSize, maxBatches }) const facts = await rebuildChannelSalesDailyFacts({ dateFrom, dateTo }) const ledger = await syncFinanceLedgerFromFacts({ dateFrom, dateTo }) const summary = await query<{ assigned: number pending: number archived: number facts: number snapshots: number }>( ` SELECT COUNT(*) FILTER (WHERE fulfillment_status = 'assigned')::int AS assigned, COUNT(*) FILTER (WHERE fulfillment_status = 'pending')::int AS pending, COUNT(*) FILTER (WHERE fulfillment_status = 'archived')::int AS archived, (SELECT COUNT(*)::int FROM sales_daily_facts) AS facts, (SELECT COUNT(*)::int FROM cost_snapshots WHERE superseded_at IS NULL) AS snapshots FROM order_items `, ) console.log( JSON.stringify( { dateFrom, dateTo, channel, product, cost, facts, ledger, totals: summary.rows[0], durationMs: Date.now() - startedAt, }, null, 2, ), ) } main() .catch((error) => { console.error(error instanceof Error ? error.message : error) process.exitCode = 1 }) .finally(async () => { await closeDb() }) async function assertSchema() { const ready = await query<{ ready: boolean }>( `SELECT to_regclass('public.sku_channel_rules') IS NOT NULL AS ready`, ) if (!ready.rows[0]?.ready) { throw new Error('缺少 112 迁移产物;请先执行 npm run db:migrate') } } function parseOptions(args: string[]) { const result: Record = {} for (const arg of args) { if (!arg.startsWith('--')) continue const body = arg.slice(2) const index = body.indexOf('=') const rawKey = index === -1 ? body : body.slice(0, index) const value = index === -1 ? 'true' : body.slice(index + 1) result[rawKey] = value result[rawKey.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase())] = value } return result } function clampInt(value: unknown, fallback: number, min: number, max: number) { const parsed = Number(value) if (!Number.isInteger(parsed)) return fallback return Math.min(max, Math.max(min, parsed)) }