feat(fulfillment): 建立正式履约影子模型
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const migration = readFileSync(
|
||||
new URL('../../db/migrations/141_fulfillment_shadow_model.sql', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const auditScript = readFileSync(
|
||||
new URL('../../../scripts/audit-fulfillment-state.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
test('正式履约影子模型包含履约、尝试和异常三张表', () => {
|
||||
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillments'))
|
||||
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillment_attempts'))
|
||||
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillment_issues'))
|
||||
assert.ok(migration.includes('uq_fulfillment_attempts_current_unit'))
|
||||
assert.ok(migration.includes('UNIQUE (fulfillment_id, issue_code)'))
|
||||
})
|
||||
|
||||
test('影子模型从现有真源回填并持续双写', () => {
|
||||
assert.ok(migration.includes('SELECT sync_fulfillment_shadow(state.order_item_id)'))
|
||||
assert.ok(migration.includes('trg_order_items_fulfillment_shadow'))
|
||||
assert.ok(migration.includes('trg_fulfillment_tasks_shadow'))
|
||||
assert.ok(migration.includes('trg_work_orders_fulfillment_shadow'))
|
||||
assert.ok(migration.includes('assign_fulfillment_event_shadow'))
|
||||
})
|
||||
|
||||
test('明确渠道即视为已路由,不用渠道配置缺失覆盖已选渠道', () => {
|
||||
assert.ok(migration.includes('v_fulfillment_item_state_raw'))
|
||||
assert.ok(migration.includes("WHEN raw.intended_channel IS NOT NULL THEN 'configured'"))
|
||||
assert.ok(
|
||||
migration.includes(
|
||||
"WHEN raw.route_status = 'missing_channel'\n AND raw.intended_channel IS NOT NULL\n THEN ''",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test('审计脚本核对影子表、执行尝试、开放异常和事件关联', () => {
|
||||
assert.ok(auditScript.includes('影子履约缺失'))
|
||||
assert.ok(auditScript.includes('影子履约状态差异'))
|
||||
assert.ok(auditScript.includes('影子执行尝试缺失'))
|
||||
assert.ok(auditScript.includes('影子开放异常缺失'))
|
||||
assert.ok(auditScript.includes('履约事件未关联影子记录'))
|
||||
})
|
||||
@@ -27,6 +27,7 @@ test(
|
||||
let orderItemId = 0
|
||||
let manualConfigId = 0
|
||||
let productId = 0
|
||||
let profileId = 0
|
||||
|
||||
try {
|
||||
const order = await query<{ id: number }>(
|
||||
@@ -51,6 +52,19 @@ test(
|
||||
)
|
||||
orderItemId = Number(item.rows[0]!.id)
|
||||
|
||||
const initialShadow = await query<{
|
||||
route_status: string
|
||||
carrier_status: string
|
||||
issue_code: string
|
||||
}>(
|
||||
`SELECT route_status, carrier_status, issue_code
|
||||
FROM fulfillments
|
||||
WHERE order_item_id = $1`,
|
||||
[orderItemId],
|
||||
)
|
||||
assert.equal(initialShadow.rows[0]?.route_status, 'unrouted')
|
||||
assert.equal(initialShadow.rows[0]?.issue_code, 'missing_channel')
|
||||
|
||||
const pendingDetail = {
|
||||
internalSku: `状态事件测试-${suffix}`,
|
||||
channel: 'manual',
|
||||
@@ -146,6 +160,33 @@ test(
|
||||
assert.equal(events[0]?.previous_channel, null)
|
||||
assert.equal(events[0]?.next_channel, 'third_party_voucher')
|
||||
|
||||
const assignedShadow = await query<{
|
||||
route_status: string
|
||||
channel: string
|
||||
carrier_status: string
|
||||
issue_code: string
|
||||
event_count: number
|
||||
}>(
|
||||
`SELECT
|
||||
f.route_status,
|
||||
f.channel,
|
||||
f.carrier_status,
|
||||
f.issue_code,
|
||||
(
|
||||
SELECT count(*)::int
|
||||
FROM fulfillment_state_events event
|
||||
WHERE event.fulfillment_id = f.id
|
||||
) AS event_count
|
||||
FROM fulfillments f
|
||||
WHERE f.order_item_id = $1`,
|
||||
[orderItemId],
|
||||
)
|
||||
assert.equal(assignedShadow.rows[0]?.route_status, 'configured')
|
||||
assert.equal(assignedShadow.rows[0]?.channel, 'third_party_voucher')
|
||||
assert.equal(assignedShadow.rows[0]?.carrier_status, 'not_required')
|
||||
assert.equal(assignedShadow.rows[0]?.issue_code, '')
|
||||
assert.equal(Number(assignedShadow.rows[0]?.event_count), 3)
|
||||
|
||||
const product = await query<{ id: number }>(
|
||||
`INSERT INTO product_catalog (
|
||||
canonical_sku, canonical_name, category, unit_name, status,
|
||||
@@ -193,6 +234,112 @@ test(
|
||||
assert.equal(Number(configured[0]?.fulfillment_config_id), manualConfigId)
|
||||
assert.equal(Number(configured[0]?.canonical_product_id), productId)
|
||||
|
||||
const profile = await query<{ id: number }>(
|
||||
`INSERT INTO fulfillment_profiles (
|
||||
profile_key, name, executor_key, requires_claim, auto_dispatch,
|
||||
inventory_strategy, config_json, created_at, updated_at
|
||||
) VALUES ($1, '状态事件任务', 'affiliate_dash', TRUE, FALSE,
|
||||
'external_platform', '{}'::jsonb, NOW(), NOW())
|
||||
RETURNING id`,
|
||||
[`ff-state-profile-${suffix}`],
|
||||
)
|
||||
profileId = Number(profile.rows[0]!.id)
|
||||
await query(
|
||||
`INSERT INTO fulfillment_tasks (
|
||||
order_id, order_item_id, unit_index, task_no, provider, platform,
|
||||
shop_id, shop_name, platform_order_id, profile_id, executor_key,
|
||||
task_status, attempt_no, is_current, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
item.order_id, item.id, 1, $2, source.provider, source.platform,
|
||||
source.shop_id, source.shop_name, source.platform_order_id, $3, 'affiliate_dash',
|
||||
'paid', 1, TRUE, NOW(), NOW()
|
||||
FROM order_items item
|
||||
JOIN orders source ON source.id = item.order_id
|
||||
WHERE item.id = $1`,
|
||||
[orderItemId, `FF-SHADOW-TASK-${suffix}`, profileId],
|
||||
)
|
||||
const attempts = await query<{
|
||||
attempt_type: string
|
||||
unit_index: number
|
||||
attempt_no: number
|
||||
source_id: number
|
||||
is_current: boolean
|
||||
}>(
|
||||
`SELECT attempt_type, unit_index, attempt_no, source_id, is_current
|
||||
FROM fulfillment_attempts
|
||||
WHERE fulfillment_id = (
|
||||
SELECT id FROM fulfillments WHERE order_item_id = $1
|
||||
)
|
||||
ORDER BY id`,
|
||||
[orderItemId],
|
||||
)
|
||||
assert.equal(attempts.rows.length, 1)
|
||||
assert.equal(attempts.rows[0]?.attempt_type, 'task')
|
||||
assert.equal(attempts.rows[0]?.unit_index, 1)
|
||||
assert.equal(attempts.rows[0]?.attempt_no, 1)
|
||||
assert.equal(attempts.rows[0]?.is_current, true)
|
||||
|
||||
const shadowDiff = await query<{
|
||||
fulfillment_state_diffs: number
|
||||
missing_attempts: number
|
||||
missing_open_issues: number
|
||||
unlinked_events: number
|
||||
}>(
|
||||
`SELECT
|
||||
COUNT(*) FILTER (
|
||||
WHERE ROW(
|
||||
fulfillment.route_status,
|
||||
fulfillment.channel,
|
||||
fulfillment.carrier_status,
|
||||
fulfillment.current_attempt_type,
|
||||
fulfillment.current_attempt_source_id,
|
||||
fulfillment.issue_code,
|
||||
fulfillment.issue_category
|
||||
) IS DISTINCT FROM ROW(
|
||||
CASE WHEN state.route_status = 'configured' THEN 'configured' ELSE 'unrouted' END,
|
||||
state.intended_channel,
|
||||
state.carrier_status,
|
||||
state.current_attempt_type,
|
||||
NULLIF(state.current_attempt_id, 0),
|
||||
state.issue_code,
|
||||
state.issue_category
|
||||
)
|
||||
)::int AS fulfillment_state_diffs,
|
||||
(
|
||||
SELECT COUNT(*)::int
|
||||
FROM fulfillment_tasks task
|
||||
WHERE task.order_item_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fulfillment_attempts attempt
|
||||
WHERE attempt.attempt_type = 'task' AND attempt.source_id = task.id
|
||||
)
|
||||
) AS missing_attempts,
|
||||
COUNT(*) FILTER (
|
||||
WHERE state.issue_code <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fulfillment_issues issue
|
||||
WHERE issue.fulfillment_id = fulfillment.id
|
||||
AND issue.issue_code = state.issue_code
|
||||
AND issue.status <> 'resolved'
|
||||
)
|
||||
)::int AS missing_open_issues,
|
||||
(
|
||||
SELECT COUNT(*)::int
|
||||
FROM fulfillment_state_events event
|
||||
WHERE event.order_item_id = $1
|
||||
AND event.fulfillment_id IS NULL
|
||||
) AS unlinked_events
|
||||
FROM v_fulfillment_item_state state
|
||||
JOIN fulfillments fulfillment ON fulfillment.order_item_id = state.order_item_id
|
||||
WHERE state.order_item_id = $1`,
|
||||
[orderItemId],
|
||||
)
|
||||
assert.equal(Number(shadowDiff.rows[0]?.fulfillment_state_diffs), 0)
|
||||
assert.equal(Number(shadowDiff.rows[0]?.missing_attempts), 0)
|
||||
assert.equal(Number(shadowDiff.rows[0]?.missing_open_issues), 0)
|
||||
assert.equal(Number(shadowDiff.rows[0]?.unlinked_events), 0)
|
||||
|
||||
await assert.rejects(
|
||||
query(`UPDATE fulfillment_state_events SET reason = 'tampered' WHERE order_item_id = $1`, [
|
||||
orderItemId,
|
||||
@@ -209,6 +356,9 @@ test(
|
||||
if (productId > 0) {
|
||||
await query(`DELETE FROM product_catalog WHERE id = $1`, [productId])
|
||||
}
|
||||
if (profileId > 0) {
|
||||
await query(`DELETE FROM fulfillment_profiles WHERE id = $1`, [profileId])
|
||||
}
|
||||
if (orderId > 0) {
|
||||
await query(`DELETE FROM orders WHERE id = $1`, [orderId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user