fix(finance): 收敛内部 SKU 履约路由与载体状态
统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。 运营指定渠道改为先创建真实载体,失败保持明确 pending。 历史缺载体、多载体冲突和自动发货兜底统一由管道修复。 迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
This commit is contained in:
@@ -11,6 +11,7 @@ type CountRow = { label: string; total: number }
|
||||
|
||||
async function main() {
|
||||
const strict = process.argv.includes('--strict')
|
||||
const includeDemo = process.argv.includes('--include-demo')
|
||||
const checks: Record<string, unknown> = {}
|
||||
|
||||
checks.migrations = (
|
||||
@@ -54,14 +55,96 @@ async function main() {
|
||||
`SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
WHERE oi.fulfillment_status = 'pending'
|
||||
AND btrim(COALESCE(oi.manual_sku_key, '')) <> ''
|
||||
AND btrim(COALESCE(resolve_internal_sku(
|
||||
oi.manual_sku_key, oi.sku_name, oi.sku_code
|
||||
), '')) <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM internal_sku_channels c
|
||||
WHERE c.enabled AND btrim(c.internal_sku) = btrim(oi.manual_sku_key)
|
||||
WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku(
|
||||
oi.manual_sku_key, oi.sku_name, oi.sku_code
|
||||
)
|
||||
)`,
|
||||
)
|
||||
).rows[0]
|
||||
|
||||
checks.assignedWithoutCarrier = (
|
||||
await query<CountRow>(
|
||||
`SELECT 'assigned_without_carrier' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel IN ('affiliate_dash', 'work_order')
|
||||
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
|
||||
AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fulfillment_tasks ft
|
||||
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
|
||||
)`,
|
||||
[includeDemo],
|
||||
)
|
||||
).rows[0]
|
||||
|
||||
checks.assignedWithMultipleCarriers = (
|
||||
await query<CountRow>(
|
||||
`SELECT 'assigned_with_multiple_carriers' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
|
||||
AND (EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
|
||||
+ (EXISTS (
|
||||
SELECT 1 FROM fulfillment_tasks ft
|
||||
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
|
||||
))::int
|
||||
+ (EXISTS (
|
||||
SELECT 1 FROM fulfillment_tasks ft
|
||||
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
|
||||
))::int > 1`,
|
||||
[includeDemo],
|
||||
)
|
||||
).rows[0]
|
||||
|
||||
checks.internalChannelConflicts = (
|
||||
await query<CountRow>(
|
||||
`SELECT internal_sku AS label, count(*)::int AS total
|
||||
FROM internal_sku_channels
|
||||
WHERE enabled = TRUE AND is_fallback = FALSE
|
||||
GROUP BY internal_sku
|
||||
HAVING count(*) > 1
|
||||
ORDER BY count(*) DESC, internal_sku`,
|
||||
)
|
||||
).rows
|
||||
|
||||
checks.enabledLegacyRules = (
|
||||
await query<CountRow>(
|
||||
`SELECT match_key_type || '/' || channel AS label, count(*)::int AS total
|
||||
FROM sku_channel_rules
|
||||
WHERE enabled = TRUE AND match_key_type = 'sku_code'
|
||||
GROUP BY 1 ORDER BY 2 DESC`,
|
||||
)
|
||||
).rows
|
||||
|
||||
checks.demoPending = (
|
||||
await query<CountRow>(
|
||||
`SELECT 'demo_pending' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'pending' AND o.shop_id LIKE 'sim-%'`,
|
||||
)
|
||||
).rows[0]
|
||||
|
||||
checks.pendingWithoutReason = (
|
||||
await query<CountRow>(
|
||||
`SELECT 'pending_without_reason' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'pending'
|
||||
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
|
||||
AND oi.fulfillment_pending_reason IS NULL`,
|
||||
[includeDemo],
|
||||
)
|
||||
).rows[0]
|
||||
|
||||
checks.channelCarrier = (
|
||||
await query<{
|
||||
channel: string
|
||||
@@ -84,8 +167,42 @@ async function main() {
|
||||
const failed: string[] = []
|
||||
const rename = checks.renameCardChannels as CountRow[]
|
||||
if (rename.some((row) => row.label === 'pending')) failed.push('11月改名卡仍有 pending')
|
||||
if ((checks.pendingWithoutChannel as CountRow).total > 0)
|
||||
if (!includeDemo && (checks.demoPending as CountRow).total > 0) {
|
||||
checks.excludedDemoPending = (checks.demoPending as CountRow).total
|
||||
}
|
||||
if ((checks.pendingWithoutChannel as CountRow).total > 0 && !includeDemo) {
|
||||
// 本地演示店铺故意保留 pending;严格模式默认只校验非演示数据。
|
||||
const productionPending = await query<CountRow>(
|
||||
`SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'pending'
|
||||
AND o.shop_id NOT LIKE 'sim-%'
|
||||
AND btrim(COALESCE(resolve_internal_sku(
|
||||
oi.manual_sku_key, oi.sku_name, oi.sku_code
|
||||
), '')) <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM internal_sku_channels c
|
||||
WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku(
|
||||
oi.manual_sku_key, oi.sku_name, oi.sku_code
|
||||
)
|
||||
)`,
|
||||
)
|
||||
if (productionPending.rows[0]!.total > 0)
|
||||
failed.push('存在有内部 SKU 但无渠道能力的非演示 pending')
|
||||
} else if ((checks.pendingWithoutChannel as CountRow).total > 0) {
|
||||
failed.push('存在有内部 SKU 但无渠道能力的 pending')
|
||||
}
|
||||
if ((checks.assignedWithoutCarrier as CountRow).total > 0)
|
||||
failed.push('存在已分配自动发货/接单渠道但没有真实载体的订单项')
|
||||
if ((checks.assignedWithMultipleCarriers as CountRow).total > 0)
|
||||
failed.push('存在同时挂载多个真实履约载体的已分配订单项')
|
||||
if ((checks.internalChannelConflicts as CountRow[]).length > 0)
|
||||
failed.push('存在多个非兜底主渠道')
|
||||
if ((checks.enabledLegacyRules as CountRow[]).length > 0)
|
||||
failed.push('仍有启用的 sku_code 兼容规则,未完全收敛到内部 SKU')
|
||||
if ((checks.pendingWithoutReason as CountRow).total > 0)
|
||||
failed.push('存在未写入 pending 原因的订单项,请先执行履约/财务管道')
|
||||
|
||||
console.log(JSON.stringify({ strict, failed, ...checks }, null, 2))
|
||||
if (strict && failed.length > 0) process.exitCode = 2
|
||||
|
||||
Reference in New Issue
Block a user