Files
order_site/apps/backend/src/services/fulfillment/fulfillment-routing.integration.test.ts
T

446 lines
18 KiB
TypeScript

/**
* 履约路由状态机集成测试(需要真实 PostgreSQL)。
*
* 运行:
* FULFILLMENT_ROUTING_TEST_DATABASE_URL="$DATABASE_URL" \
* npx tsx --test src/services/fulfillment/fulfillment-routing.integration.test.ts
*
* 未设置环境变量时跳过,普通 npm test 不依赖数据库。
*/
import assert from 'node:assert/strict'
import test from 'node:test'
const TEST_DATABASE_URL = String(process.env.FULFILLMENT_ROUTING_TEST_DATABASE_URL || '').trim()
test(
'运营指定渠道:先保证载体;支持安全切换;缺载体保持 pending',
{ skip: !TEST_DATABASE_URL ? '未设置 FULFILLMENT_ROUTING_TEST_DATABASE_URL' : false },
async () => {
process.env.DATABASE_URL = TEST_DATABASE_URL
const { closeDb, query } = await import('../../db/client.js')
const {
assignChannelByInternalSku,
deleteChannelRules,
ensureVoucherRuleForSku,
listChannelRules,
listChannelRulesPage,
listPendingChannelGroups,
switchChannelForOrderItems,
upsertChannelRule,
} = await import('./channel-assign-service.js')
const { runAffiliateFallback } = await import('./affiliate-fallback-service.js')
const {
deleteInternalSkuMapping,
listInternalSkuMappingsPage,
resolveInternalSkuMapping,
upsertInternalSkuMapping,
} = await import('./internal-sku-mapping-service.js')
const { updateOrderItemSourceSnapshot } = await import('../../repositories/order-item-repo.js')
const suffix = `${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
const shopId = `it-routing-${suffix}`
const manualSku = `集成测试人工-${suffix}`
const missingAffiliateSku = `集成测试自动-${suffix}`
const voucherSku = `集成测试卡券-${suffix}`
const triggerSku = `集成测试规范-${suffix}`
const fallbackSku = `集成测试兜底-${suffix}`
const orderIds: number[] = []
const orderItemIds: number[] = []
const crossShopSku = `集成测试跨店-${suffix}`
const internalSkus = [
manualSku,
missingAffiliateSku,
voucherSku,
crossShopSku,
triggerSku,
fallbackSku,
]
let generatedVoucherProductId: number | null = null
let fallbackProfileId: number | null = null
async function createPendingOrderItem(
sku: string,
options: { provider?: string; platform?: string; shopId?: string } = {},
) {
const provider = options.provider || '91kaquan'
const platform = options.platform || 'kuaishou-91'
const targetShopId = options.shopId || shopId
const order = await query<{ id: number }>(
`INSERT INTO orders (
provider, platform, shop_id, platform_order_id, order_status, pay_status,
total_amount, paid_at, created_at, updated_at
) VALUES (
$1, $2, $3, $4, 'paid', 'paid', 1000, NOW(), NOW(), NOW()
) RETURNING id`,
[provider, platform, targetShopId, `IT-ROUTING-${suffix}-${orderItemIds.length + 1}`],
)
const orderId = Number(order.rows[0]!.id)
orderIds.push(orderId)
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, $3::jsonb, 1000, 'order_total', NOW(), NOW())
RETURNING id`,
[orderId, sku, JSON.stringify({ skuNick: sku })],
)
const orderItemId = Number(item.rows[0]!.id)
orderItemIds.push(orderItemId)
return orderItemId
}
try {
const canonicalResolution = await query<{
internal_sku: string | null
platform_only: string | null
cjk_extension: string | null
}>(
`SELECT
resolve_internal_sku('platform-code', ' 套装-浪漫 天命 ', 'platform-code') AS internal_sku,
resolve_internal_sku('platform-code', 'Platform Title', 'platform-code') AS platform_only,
resolve_internal_sku('', '𠀀', '') AS cjk_extension`,
)
assert.equal(canonicalResolution.rows[0]?.internal_sku, '套装-浪漫天命')
assert.equal(canonicalResolution.rows[0]?.platform_only, null)
assert.equal(canonicalResolution.rows[0]?.cjk_extension, '𠀀')
await query(
`INSERT INTO internal_sku_channels (internal_sku, channel, priority, is_fallback, enabled, source)
VALUES ($1, 'manual', 90, TRUE, TRUE, 'integration_test')`,
[` 集成测试 规范-${suffix} `],
)
const normalizedInsert = await query<{ internal_sku: string }>(
`SELECT internal_sku FROM internal_sku_channels WHERE source = 'integration_test'`,
)
assert.equal(normalizedInsert.rows[0]?.internal_sku, triggerSku)
const mappingValue = `MAP-${suffix}`
const mappingSku = `集成映射-${suffix}`
const mappingId = await upsertInternalSkuMapping({
provider: '91kaquan',
shopId,
matchType: 'sku_code',
matchValue: ` ${mappingValue} `,
internalSku: mappingSku,
priority: 10,
source: 'integration_test',
note: '集成测试映射',
})
const resolvedMapping = await resolveInternalSkuMapping({
provider: '91kaquan',
shopId,
candidates: [{ matchType: 'sku_code', value: mappingValue }],
})
assert.equal(resolvedMapping?.internalSku, mappingSku)
const mappingPage = await listInternalSkuMappingsPage({
keyword: mappingSku,
page: 1,
pageSize: 10,
})
assert.equal(mappingPage.total, 1)
assert.equal(await deleteInternalSkuMapping(mappingId), true)
const manualItemId = await createPendingOrderItem(manualSku)
await query(
`INSERT INTO manual_fulfillment_configs (
sku_key, platform_key, credit_quantity, cost_mode, enabled, note, created_at, updated_at
) VALUES ($1, 'nanhang', 0, 'variable', TRUE, '集成测试', NOW(), NOW())`,
[manualSku],
)
const manualAssigned = await assignChannelByInternalSku({
internalSku: manualSku,
channel: 'manual',
adminUserId: null,
note: '集成测试人工',
})
assert.equal(manualAssigned.updated, 1)
assert.equal(manualAssigned.carrierFailed, 0)
const manualItem = await query<{
fulfillment_status: string
fulfillment_channel: string
fulfillment_config_id: number | null
}>(
`SELECT fulfillment_status, fulfillment_channel, fulfillment_config_id
FROM order_items WHERE id = $1`,
[manualItemId],
)
assert.equal(manualItem.rows[0]?.fulfillment_status, 'assigned')
assert.equal(manualItem.rows[0]?.fulfillment_channel, 'manual')
assert.ok(Number(manualItem.rows[0]?.fulfillment_config_id) > 0)
const sourceSyncedItem = await updateOrderItemSourceSnapshot(manualItemId, {
skuName: `平台覆盖-${suffix}`,
specJson: JSON.stringify({ skuNick: `平台覆盖-${suffix}` }),
itemSnapshotJson: JSON.stringify({ externalSkuName: `平台覆盖-${suffix}` }),
updatedAt: new Date().toISOString(),
})
assert.equal(sourceSyncedItem?.sku_name, manualSku)
assert.equal(sourceSyncedItem?.spec_json?.skuNick, manualSku)
const switched = await switchChannelForOrderItems({
orderItemIds: [manualItemId],
channel: 'third_party_voucher',
adminUserId: null,
note: '集成测试切换',
})
assert.equal(switched.switched, 1)
assert.equal(switched.failed, 0)
const switchedItem = await query<{
fulfillment_status: string
fulfillment_channel: string
original_channel: string | null
canonical_product_id: number | null
canonical_sku: string | null
item_snapshot_json: Record<string, unknown>
}>(
`SELECT oi.fulfillment_status, oi.fulfillment_channel, oi.original_channel,
oi.canonical_product_id, product.canonical_sku, oi.item_snapshot_json
FROM order_items oi
LEFT JOIN product_catalog product ON product.id = oi.canonical_product_id
WHERE oi.id = $1`,
[manualItemId],
)
assert.equal(switchedItem.rows[0]?.fulfillment_status, 'assigned')
assert.equal(switchedItem.rows[0]?.fulfillment_channel, 'third_party_voucher')
assert.equal(switchedItem.rows[0]?.original_channel, 'manual')
assert.ok(switchedItem.rows[0]?.canonical_sku?.startsWith('tpv:'))
assert.equal(
switchedItem.rows[0]?.item_snapshot_json.internalSkuChannel,
'third_party_voucher',
)
assert.equal(switchedItem.rows[0]?.item_snapshot_json.isConfigured, false)
generatedVoucherProductId = Number(switchedItem.rows[0]?.canonical_product_id || 0) || null
const missingAffiliateItemId = await createPendingOrderItem(missingAffiliateSku)
const affiliateFailed = await assignChannelByInternalSku({
internalSku: missingAffiliateSku,
channel: 'affiliate_dash',
adminUserId: null,
note: '集成测试缺映射',
})
assert.equal(affiliateFailed.updated, 0)
assert.equal(affiliateFailed.carrierFailed, 1)
const pendingItem = await query<{
fulfillment_status: string
fulfillment_channel: string | null
fulfillment_pending_reason: string | null
}>(
`SELECT fulfillment_status, fulfillment_channel, fulfillment_pending_reason
FROM order_items WHERE id = $1`,
[missingAffiliateItemId],
)
assert.equal(pendingItem.rows[0]?.fulfillment_status, 'pending')
assert.equal(pendingItem.rows[0]?.fulfillment_channel, null)
assert.equal(pendingItem.rows[0]?.fulfillment_pending_reason, 'no_carrier')
const crossShopA = `it-routing-a-${suffix}`
const crossShopB = `it-routing-b-${suffix}`
await createPendingOrderItem(crossShopSku, { shopId: crossShopA })
await createPendingOrderItem(crossShopSku, {
provider: 'pdd',
platform: 'pdd',
shopId: crossShopB,
})
const pendingGroups = await listPendingChannelGroups({
keyword: crossShopSku,
page: 1,
pageSize: 10,
})
assert.equal(pendingGroups.total, 1)
assert.equal(pendingGroups.items[0]?.internal_sku, crossShopSku)
assert.equal(pendingGroups.items[0]?.item_count, 2)
assert.deepEqual(pendingGroups.items[0]?.providers.sort(), ['91kaquan', 'pdd'])
assert.deepEqual(pendingGroups.items[0]?.shop_ids.sort(), [crossShopA, crossShopB])
const fallbackItemId = await createPendingOrderItem(fallbackSku)
await query(
`INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled, config_json, source
) VALUES
($1, 'affiliate_dash', 10, FALSE, TRUE, '{}'::jsonb, 'integration_test'),
($1, 'manual', 90, TRUE, TRUE, '{}'::jsonb, 'integration_test')`,
[fallbackSku],
)
const fallbackItem = await query<{ order_id: number }>(
`UPDATE order_items
SET fulfillment_status = 'assigned', fulfillment_channel = 'affiliate_dash',
fulfillment_source = 'operator_assigned', fulfillment_locked = FALSE,
updated_at = NOW()
WHERE id = $1
RETURNING order_id`,
[fallbackItemId],
)
const fallbackOrderId = Number(fallbackItem.rows[0]!.order_id)
const fallbackProfile = 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,
'static_pool', '{}'::jsonb, NOW(), NOW())
RETURNING id`,
[`it-affiliate-${suffix}`],
)
fallbackProfileId = Number(fallbackProfile.rows[0]!.id)
const fallbackOrder = await query<{
provider: string
platform: string
shop_id: string
shop_name: string
platform_order_id: string
}>(
`SELECT provider, platform, shop_id, shop_name, platform_order_id
FROM orders WHERE id = $1`,
[fallbackOrderId],
)
const orderRow = fallbackOrder.rows[0]!
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, created_at, updated_at
) VALUES ($1, $2, 1, $3, $4, $5, $6, $7, $8, $9, 'affiliate_dash', 'failed', NOW(), NOW())`,
[
fallbackOrderId,
fallbackItemId,
`IT-AFF-FAIL-${suffix}`,
orderRow.provider,
orderRow.platform,
orderRow.shop_id,
orderRow.shop_name,
orderRow.platform_order_id,
fallbackProfileId,
],
)
const fallbackWithoutConfig = await runAffiliateFallback({ limit: 50 })
assert.equal(fallbackWithoutConfig.switched, 0)
assert.equal(
(
await query<{ fulfillment_channel: string }>(
`SELECT fulfillment_channel FROM order_items WHERE id = $1`,
[fallbackItemId],
)
).rows[0]?.fulfillment_channel,
'affiliate_dash',
)
const manualFallbackConfig = await query<{ id: number }>(
`INSERT INTO manual_fulfillment_configs (
sku_key, platform_key, credit_quantity, cost_mode, enabled, created_at, updated_at
) VALUES ($1, 'nanhang', 0, 'variable', TRUE, NOW(), NOW())
RETURNING id`,
[fallbackSku],
)
const fallbackWithConfig = await runAffiliateFallback({ limit: 50 })
assert.equal(fallbackWithConfig.switched, 1)
const fallbackAssigned = await query<{
fulfillment_channel: string
fulfillment_config_id: number | null
item_snapshot_json: Record<string, unknown>
}>(
`SELECT fulfillment_channel, fulfillment_config_id, item_snapshot_json
FROM order_items WHERE id = $1`,
[fallbackItemId],
)
assert.equal(fallbackAssigned.rows[0]?.fulfillment_channel, 'manual')
assert.equal(fallbackAssigned.rows[0]?.item_snapshot_json.internalSkuChannel, 'manual')
assert.equal(fallbackAssigned.rows[0]?.item_snapshot_json.isConfigured, true)
assert.equal(
Number(fallbackAssigned.rows[0]?.fulfillment_config_id),
Number(manualFallbackConfig.rows[0]!.id),
)
await ensureVoucherRuleForSku(voucherSku)
const voucherChannel = await query<{ channel: string; source: string }>(
`SELECT channel, source FROM internal_sku_channels
WHERE internal_sku = $1 ORDER BY channel`,
[voucherSku],
)
assert.deepEqual(voucherChannel.rows, [{ channel: 'third_party_voucher', source: 'voucher' }])
const legacyVoucherRules = await query<{ count: number }>(
`SELECT count(*)::int AS count FROM sku_channel_rules
WHERE match_key = $1`,
[voucherSku],
)
assert.equal(legacyVoucherRules.rows[0]?.count, 0)
const savedRule = await upsertChannelRule({
matchKeyType: 'internal_sku',
matchKey: voucherSku,
channel: 'third_party_voucher',
priority: 10,
enabled: true,
note: '集成测试统一渠道',
adminUserId: null,
})
const listedRules = await listChannelRules({ channel: 'third_party_voucher', enabled: true })
assert.ok(
listedRules.some((rule) => rule.id === savedRule.id && rule.match_key === voucherSku),
)
const firstRulePage = await listChannelRulesPage({
channel: 'third_party_voucher',
enabled: true,
page: 1,
pageSize: 2,
})
assert.equal(firstRulePage.page, 1)
assert.equal(firstRulePage.pageSize, 2)
assert.ok(firstRulePage.items.length <= 2)
assert.equal(firstRulePage.total, listedRules.length)
const deletedRules = await deleteChannelRules([savedRule.id])
assert.equal(deletedRules.deletedCount, 1)
assert.deepEqual(deletedRules.deletedIds, [savedRule.id])
} finally {
if (orderItemIds.length > 0) {
await query(`DELETE FROM cost_exception_items WHERE order_item_id = ANY($1::bigint[])`, [
orderItemIds,
])
await query(`DELETE FROM cost_snapshots WHERE order_item_id = ANY($1::bigint[])`, [
orderItemIds,
])
await query(`DELETE FROM order_items WHERE id = ANY($1::bigint[])`, [orderItemIds])
}
if (orderIds.length > 0) {
await query(`DELETE FROM orders WHERE id = ANY($1::bigint[])`, [orderIds])
}
await query(`DELETE FROM cost_exceptions WHERE scope_key LIKE $1`, [`%${suffix}%`])
await query(`DELETE FROM manual_fulfillment_configs WHERE sku_key = ANY($1::text[])`, [
internalSkus,
])
await query(
`DELETE FROM internal_sku_mappings WHERE provider = '91kaquan' AND shop_id = $1`,
[shopId],
)
await query(`DELETE FROM internal_sku_channels WHERE internal_sku = ANY($1::text[])`, [
internalSkus,
])
await query(
`DELETE FROM sku_channel_rules
WHERE provider = '91kaquan' AND shop_id = $1 AND match_key = ANY($2::text[])`,
[shopId, internalSkus],
)
if (generatedVoucherProductId) {
await query(`DELETE FROM sales_daily_facts WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM cost_snapshots WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM channel_product_mappings WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM cost_versions WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM product_catalog WHERE id = $1 AND canonical_sku = $2`, [
generatedVoucherProductId,
`tpv:${manualSku}`,
])
}
if (fallbackProfileId) {
await query(`DELETE FROM fulfillment_profiles WHERE id = $1`, [fallbackProfileId])
}
await closeDb()
}
},
)