369 lines
14 KiB
TypeScript
369 lines
14 KiB
TypeScript
/**
|
|
* 履约状态写入口集成测试(需要已执行完全部迁移的 PostgreSQL)。
|
|
*
|
|
* FULFILLMENT_STATE_TEST_DATABASE_URL=postgres://... \
|
|
* npx tsx --test src/services/fulfillment/fulfillment-state-service.integration.test.ts
|
|
*/
|
|
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
const TEST_DATABASE_URL = String(process.env.FULFILLMENT_STATE_TEST_DATABASE_URL || '').trim()
|
|
|
|
test(
|
|
'履约状态事件覆盖状态写入口、遗留直写,并保持变更幂等',
|
|
{ skip: !TEST_DATABASE_URL ? '未设置 FULFILLMENT_STATE_TEST_DATABASE_URL' : false },
|
|
async () => {
|
|
process.env.DATABASE_URL = TEST_DATABASE_URL
|
|
const { closeDb, query } = await import('../../db/client.js')
|
|
const {
|
|
assignFulfillmentChannelsWithExecutor,
|
|
attachFulfillmentConfigurations,
|
|
listFulfillmentStateEvents,
|
|
runWithFulfillmentStateTransaction,
|
|
transitionFulfillmentState,
|
|
} = await import('./fulfillment-state-service.js')
|
|
const suffix = `${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
|
|
let orderId = 0
|
|
let orderItemId = 0
|
|
let manualConfigId = 0
|
|
let productId = 0
|
|
let profileId = 0
|
|
|
|
try {
|
|
const order = await query<{ id: number }>(
|
|
`INSERT INTO orders (
|
|
provider, platform, shop_id, shop_name, platform_order_id,
|
|
order_status, pay_status, total_amount, paid_at, created_at, updated_at
|
|
) VALUES (
|
|
'91kaquan', 'kuaishou-91', $1, '状态事件集成测试', $2,
|
|
'paid', 'paid', 1000, NOW(), NOW(), NOW()
|
|
)
|
|
RETURNING id`,
|
|
[`ff-state-${suffix}`, `FF-STATE-${suffix}`],
|
|
)
|
|
orderId = Number(order.rows[0]!.id)
|
|
const item = await query<{ id: number }>(
|
|
`INSERT INTO order_items (
|
|
order_id, sku_code, sku_name, quantity, spec_json,
|
|
revenue_amount_fen, revenue_basis, created_at, updated_at
|
|
) VALUES ($1, $2, $2, 1, '{}'::jsonb, 1000, 'order_total', NOW(), NOW())
|
|
RETURNING id`,
|
|
[orderId, `状态事件测试-${suffix}`],
|
|
)
|
|
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',
|
|
carrierIssue: 'manual_config_missing',
|
|
}
|
|
const first = await transitionFulfillmentState({
|
|
orderItemId,
|
|
expected: { status: 'pending' },
|
|
patch: {
|
|
pendingReason: 'no_carrier',
|
|
pendingDetail,
|
|
},
|
|
eventType: 'test_carrier_pending',
|
|
actor: { type: 'operator', id: 42 },
|
|
reason: 'integration_test',
|
|
})
|
|
assert.equal(first?.fulfillment_pending_reason, 'no_carrier')
|
|
|
|
let events = await listFulfillmentStateEvents(orderItemId)
|
|
assert.equal(events.length, 1)
|
|
assert.equal(events[0]?.event_type, 'test_carrier_pending')
|
|
assert.equal(events[0]?.actor_type, 'operator')
|
|
assert.equal(events[0]?.actor_id, '42')
|
|
assert.equal(events[0]?.reason, 'integration_test')
|
|
assert.deepEqual(events[0]?.next_pending_detail, pendingDetail)
|
|
|
|
await transitionFulfillmentState({
|
|
orderItemId,
|
|
expected: { status: 'pending' },
|
|
patch: {
|
|
pendingReason: 'no_carrier',
|
|
pendingDetail,
|
|
},
|
|
eventType: 'test_same_state_replay',
|
|
})
|
|
events = await listFulfillmentStateEvents(orderItemId)
|
|
assert.equal(events.length, 1, '相同状态重复写入不能制造第二条事件')
|
|
|
|
const stale = await transitionFulfillmentState({
|
|
orderItemId,
|
|
expected: { status: 'assigned' },
|
|
patch: {
|
|
channel: 'manual',
|
|
status: 'assigned',
|
|
},
|
|
eventType: 'test_stale_transition',
|
|
})
|
|
assert.equal(stale, null, '期望状态不一致时必须拒绝写入')
|
|
assert.equal((await listFulfillmentStateEvents(orderItemId)).length, 1)
|
|
|
|
await query(
|
|
`UPDATE order_items
|
|
SET fulfillment_pending_detail = jsonb_build_object('legacyWrite', TRUE),
|
|
updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[orderItemId],
|
|
)
|
|
events = await listFulfillmentStateEvents(orderItemId)
|
|
assert.equal(events.length, 2, '遗留直写也必须由触发器留痕')
|
|
assert.equal(events[0]?.event_type, 'fulfillment_state_changed')
|
|
assert.equal(events[0]?.actor_type, 'system')
|
|
|
|
await query(
|
|
`UPDATE order_items
|
|
SET fulfillment_pending_detail = jsonb_build_object('legacyWrite', TRUE),
|
|
updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[orderItemId],
|
|
)
|
|
events = await listFulfillmentStateEvents(orderItemId)
|
|
assert.equal(events.length, 2, '无字段变化的更新不能再记事件')
|
|
|
|
await runWithFulfillmentStateTransaction(async (client) => {
|
|
const assigned = await assignFulfillmentChannelsWithExecutor(client, {
|
|
assignments: [
|
|
{
|
|
orderItemId,
|
|
channel: 'third_party_voucher',
|
|
source: 'auto_mapping',
|
|
},
|
|
],
|
|
eventType: 'test_channel_assigned',
|
|
reason: 'integration_test',
|
|
})
|
|
assert.equal(assigned.length, 1)
|
|
assert.equal(assigned[0]?.fulfillment_status, 'assigned')
|
|
assert.equal(assigned[0]?.fulfillment_channel, 'third_party_voucher')
|
|
})
|
|
events = await listFulfillmentStateEvents(orderItemId)
|
|
assert.equal(events[0]?.event_type, 'test_channel_assigned')
|
|
assert.equal(events[0]?.previous_status, 'pending')
|
|
assert.equal(events[0]?.next_status, 'assigned')
|
|
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,
|
|
attributes_json, created_at, updated_at
|
|
) VALUES ($1, $2, '测试', '件', 'active', '{}'::jsonb, NOW(), NOW())
|
|
RETURNING id`,
|
|
[`test:ff-state-${suffix}`, `状态事件商品-${suffix}`],
|
|
)
|
|
productId = Number(product.rows[0]!.id)
|
|
const config = await query<{ id: number }>(
|
|
`INSERT INTO manual_fulfillment_configs (
|
|
sku_key, canonical_product_id, platform_key, credit_quantity,
|
|
cost_mode, enabled, note, created_at, updated_at
|
|
) VALUES ($1, $2, 'nanhang', 1, 'fixed', TRUE, 'integration_test', NOW(), NOW())
|
|
RETURNING id`,
|
|
[`状态事件测试-${suffix}`, productId],
|
|
)
|
|
manualConfigId = Number(config.rows[0]!.id)
|
|
const staleConfig = await attachFulfillmentConfigurations({
|
|
mappings: [
|
|
{
|
|
orderItemId,
|
|
configId: manualConfigId,
|
|
canonicalProductId: productId,
|
|
expectedCanonicalProductId: manualConfigId,
|
|
},
|
|
],
|
|
onlyWhenCanonicalProductNull: true,
|
|
})
|
|
assert.equal(staleConfig.length, 0, '原商品值已变化时不能覆盖并发结果')
|
|
|
|
const configured = await attachFulfillmentConfigurations({
|
|
mappings: [
|
|
{
|
|
orderItemId,
|
|
configId: manualConfigId,
|
|
canonicalProductId: productId,
|
|
},
|
|
],
|
|
expected: { status: 'assigned', channel: 'third_party_voucher' },
|
|
onlyWhenConfigNull: true,
|
|
onlyWhenCanonicalProductNull: true,
|
|
})
|
|
assert.equal(configured.length, 1)
|
|
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,
|
|
]),
|
|
/append-only/,
|
|
)
|
|
} finally {
|
|
if (orderItemId > 0) {
|
|
await query(`DELETE FROM order_items WHERE id = $1`, [orderItemId])
|
|
}
|
|
if (manualConfigId > 0) {
|
|
await query(`DELETE FROM manual_fulfillment_configs WHERE id = $1`, [manualConfigId])
|
|
}
|
|
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])
|
|
}
|
|
await closeDb()
|
|
}
|
|
},
|
|
)
|