diff --git a/apps/backend/src/db/migrations/113_finance_channel_cutover.sql b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql index ba6abc50..096211ae 100644 --- a/apps/backend/src/db/migrations/113_finance_channel_cutover.sql +++ b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql @@ -404,6 +404,19 @@ COMMENT ON TABLE finance_watermarks IS -- 先沿用旧列名承载渠道值,本迁移末尾统一改名为 channel; -- 这里先放宽约束,允许卡券、未分类与归档状态。 +-- 断代前的销售事实仍是旧口径,可能残留 legacy_platform;新约束不允许该值。 +-- 这些事实随后由 finance:cutover-v2 整体清空,这里先归入 archived(未知值归入 unclassified), +-- 保证约束能加上;否则生产基线 112 会因存量行直接失败。 +UPDATE sales_daily_facts +SET fulfillment_mode = CASE + WHEN fulfillment_mode = 'legacy_platform' THEN 'archived' + ELSE 'unclassified' +END +WHERE fulfillment_mode NOT IN ( + 'work_order', 'affiliate_dash', 'third_party_voucher', + 'manual', 'unclassified', 'archived' +); + ALTER TABLE sales_daily_facts DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_fulfillment_mode; diff --git a/apps/backend/src/services/finance/finance-cutover-migration.test.ts b/apps/backend/src/services/finance/finance-cutover-migration.test.ts new file mode 100644 index 00000000..9645234f --- /dev/null +++ b/apps/backend/src/services/finance/finance-cutover-migration.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import test from 'node:test' + +const migration = readFileSync( + fileURLToPath(new URL('../../db/migrations/113_finance_channel_cutover.sql', import.meta.url)), + 'utf8', +) + +test('113 迁移必须先归一销售事实旧口径,再收紧渠道约束', () => { + const updateIndex = migration.indexOf('UPDATE sales_daily_facts') + const constraintIndex = migration.indexOf('ADD CONSTRAINT chk_sales_daily_facts_fulfillment_mode') + assert.ok(updateIndex > 0, '缺少 sales_daily_facts 旧口径归一') + assert.ok(constraintIndex > updateIndex, '必须先归一数据,再加新约束') + assert.ok( + migration.includes("WHEN fulfillment_mode = 'legacy_platform' THEN 'archived'"), + 'legacy_platform 必须归入 archived', + ) + assert.ok( + migration.includes("ELSE 'unclassified'"), + '其它未知旧值必须归入 unclassified,避免约束因脏数据失败', + ) + for (const channel of [ + 'work_order', + 'affiliate_dash', + 'third_party_voucher', + 'manual', + 'unclassified', + 'archived', + ]) { + assert.ok(migration.includes(`'${channel}'`), `新约束必须允许 ${channel}`) + } +})