100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import { readFile } from 'node:fs/promises'
|
||
import test from 'node:test'
|
||
|
||
async function readJobSource() {
|
||
return readFile(new URL('./data-retention-job.ts', import.meta.url), 'utf8')
|
||
}
|
||
|
||
test('数据维护不得覆盖原始报文或删除永久业务事实', async () => {
|
||
const src = await readJobSource()
|
||
assert.equal(src.includes('raw_payload_json ='), false)
|
||
assert.equal(src.includes('DELETE FROM'), false)
|
||
for (const table of [
|
||
'orders',
|
||
'kuaishou_industry_vouchers',
|
||
'task_events',
|
||
'webhook_events',
|
||
'admin_audit_logs',
|
||
'work_order_events',
|
||
]) {
|
||
assert.equal(
|
||
new RegExp(`(?:UPDATE|DELETE)\\s+${table}\\b`, 'i').test(src),
|
||
false,
|
||
`${table} 属于永久业务事实,不能在维护任务中清理或覆盖`,
|
||
)
|
||
}
|
||
assert.ok(src.includes('永久业务事实未清理'))
|
||
})
|
||
|
||
test('成本异常只归档已处理的行,open 队列不能被藏起来', async () => {
|
||
const job = await readJobSource()
|
||
assert.ok(
|
||
job.includes('archiveResolvedCostExceptions'),
|
||
'成本异常必须走归档,不能直接 DELETE 掉审计痕迹',
|
||
)
|
||
assert.ok(job.includes('retention.costExceptionDays'), '保留天数必须走配置项')
|
||
|
||
// 归档 SQL 在独立服务里,检查服务源码而不是任务源码。
|
||
const service = await readFile(
|
||
new URL('../cost-center/cost-exception-archive-service.ts', import.meta.url),
|
||
'utf8',
|
||
)
|
||
assert.ok(service.includes("status <> 'open'"), 'open 行是待办队列,不能因超期被归档')
|
||
assert.ok(
|
||
service.includes('COALESCE(resolved_at, created_at)'),
|
||
'归档时钟优先用 resolved_at,ignored 行没有 resolved_at 时回退 created_at',
|
||
)
|
||
assert.ok(
|
||
service.includes('INSERT INTO cost_exceptions_archive'),
|
||
'归档必须落到 cost_exceptions_archive,保持可追溯',
|
||
)
|
||
})
|
||
|
||
test('保留任务要回填漏写的付款派生列,并在有命中时打日志', async () => {
|
||
const src = await readJobSource()
|
||
assert.ok(
|
||
src.includes('source_order_amount_fen IS NULL'),
|
||
'只处理派生列为空的行,避免把合法 0 或已落列的金额改掉',
|
||
)
|
||
assert.ok(
|
||
src.includes("material_json->'source'->>'orderAmountFen'"),
|
||
'回填口径必须与 083 / resolveSourceOrderAmountFen 相同',
|
||
)
|
||
assert.ok(src.includes("~ '^-?[0-9]{1,15}$'"), '超长数字不能 ::bigint,否则整次保留任务失败')
|
||
assert.ok(
|
||
src.includes('source_order_amount_fen 出现漏写'),
|
||
'有命中必须打日志,否则金额静默回退 orders.total_amount',
|
||
)
|
||
})
|
||
|
||
test('数据维护只保留可派生数据的成本异常归档窗口', async () => {
|
||
const defaults = await readFile(new URL('../../config/defaults.ts', import.meta.url), 'utf8')
|
||
assert.ok(defaults.includes('costExceptionDays: 60'))
|
||
for (const line of [
|
||
'rawPayloadDays:',
|
||
'taskEventDays:',
|
||
'webhookEventDays:',
|
||
'auditLogDays:',
|
||
'workOrderEventDays:',
|
||
]) {
|
||
assert.equal(defaults.includes(line), false, `永久事实不能存在清理窗口 ${line}`)
|
||
}
|
||
|
||
const overrides = await readFile(
|
||
new URL('../../config/env-overrides.ts', import.meta.url),
|
||
'utf8',
|
||
)
|
||
assert.equal(overrides.includes('RAW_PAYLOAD_RETENTION_DAYS'), false)
|
||
assert.equal(overrides.includes('TASK_EVENT_RETENTION_DAYS'), false)
|
||
assert.equal(overrides.includes('WEBHOOK_EVENT_RETENTION_DAYS'), false)
|
||
assert.equal(overrides.includes('AUDIT_LOG_RETENTION_DAYS'), false)
|
||
assert.equal(overrides.includes('WORK_ORDER_EVENT_RETENTION_DAYS'), false)
|
||
assert.ok(
|
||
overrides.includes(
|
||
"integerEnv('COST_EXCEPTION_RETENTION_DAYS', ['retention', 'costExceptionDays'])",
|
||
),
|
||
'COST_EXCEPTION_RETENTION_DAYS 需要环境变量出口,否则生产无法调整归档窗口',
|
||
)
|
||
})
|