import assert from 'node:assert/strict' import test from 'node:test' import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js' import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' import type { JsonObject } from '../../types/json.js' type CreatedTaskInput = { orderId?: number orderItemId?: number unitIndex?: number contextJson?: string taskStatus?: string executorKey?: string [key: string]: unknown } function stubOrder( patch: Partial & Pick, ): OrderRow { return { id: patch.id, order_id: patch.id, provider: patch.provider || 'open_91', platform: patch.platform || 'kuaishou', shop_id: patch.shop_id || 'shop-1', shop_name: patch.shop_name || '测试店铺', platform_order_id: patch.platform_order_id, order_status: patch.order_status || 'created', pay_status: patch.pay_status || 'pending_payment', buyer_id: '', buyer_name: '', receiver_contact: '', total_amount: 0, currency: 'CNY', raw_payload_json: '{}', paid_at: null, created_at: '2026-05-29T00:00:00.000Z', updated_at: '2026-05-29T00:00:00.000Z', ...patch, } as OrderRow } function stubOrderItem( patch: Partial & Pick, ): OrderItemRow { return { id: patch.id, order_id: patch.order_id, sku_code: patch.sku_code, sku_name: patch.sku_name || patch.sku_code, quantity: patch.quantity || 1, spec_json: '{}', item_snapshot_json: patch.item_snapshot_json || {}, created_at: '2026-05-29T00:00:00.000Z', updated_at: '2026-05-29T00:00:00.000Z', ...patch, } as OrderItemRow } function stubCreatedTask(id: number, input: CreatedTaskInput): TaskRow { return { id, order_id: Number(input.orderId || 0), order_item_id: Number(input.orderItemId || 0), unit_index: Number(input.unitIndex || 1), platform_order_id: '', profile_id: 0, task_no: 'DT-test', executor_key: String(input.executorKey || ''), task_status: String(input.taskStatus || 'pending'), delivery_status: 'pending', result_code: '', result_message: '', automation_mode: 'manual', requires_claim: true, user_action_status: 'pending_claim', attempt_count: 0, runtime_session_id: '', login_type: '', nickname: '', role_name: '', role_id: '', area: '', partition_name: '', claim_token: '', primary_claim_token: '', primary_claim_token_id: null, primary_claim_token_status: '', artifacts_json: '{}', context_json: String(input.contextJson || '{}'), screenshot_path: '', last_error: '', retry_count: 0, created_at: '2026-05-29T00:00:00.000Z', updated_at: '2026-05-29T00:00:00.000Z', claimed_at: null, role_confirmed_at: null, redeemed_at: null, } as TaskRow } test('syncDeliveryTasksForOrder 为 affiliate_dash 商品创建任务上下文', async () => { const createdInputs: CreatedTaskInput[] = [] await syncDeliveryTasksForOrderWithDeps( stubOrder({ id: 1, pay_status: 'pending_payment', platform_order_id: '2614900602069169', }), [ stubOrderItem({ id: 10, order_id: 1, sku_code: 'AFF-1001', sku_name: '幸运币90个', quantity: 1, item_snapshot_json: buildAffiliateSnapshot('AFF-1001', '幸运币90个'), }), ], { listTasksByOrderId: async () => [], getFulfillmentProfileByKey: async () => buildAffiliateProfile(), createTask: async (input) => { createdInputs.push(input as CreatedTaskInput) return stubCreatedTask(100, input as CreatedTaskInput) }, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-test', createTaskEvent: async () => null, }, ) assert.equal(createdInputs[0]?.executorKey, 'affiliate_dash') const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject const affiliateDash = context.affiliateDash as JsonObject assert.equal(affiliateDash.sku, 'AFF-1001') assert.equal(affiliateDash.productName, '幸运币90个') assert.equal(affiliateDash.flowType, 'affiliate_dash') }) test('syncDeliveryTasksForOrder 已有部分任务时按订单项补建缺失任务', async () => { const existingTask = stubCreatedTask(201, { orderId: 3, orderItemId: 20, unitIndex: 1, executorKey: 'affiliate_dash', }) const createdInputs: CreatedTaskInput[] = [] const taskEvents: Array<{ taskId: number; eventType: string }> = [] const tasks = await syncDeliveryTasksForOrderWithDeps( stubOrder({ id: 3, platform_order_id: 'partial-order' }), [ stubOrderItem({ id: 20, order_id: 3, sku_code: 'SKU-existing', item_snapshot_json: buildAffiliateSnapshot('AFF-A', '商品 A'), }), stubOrderItem({ id: 21, order_id: 3, sku_code: 'SKU-missing', item_snapshot_json: buildAffiliateSnapshot('AFF-B', '商品 B'), }), ], { listTasksByOrderId: async () => [existingTask], getFulfillmentProfileByKey: async () => buildAffiliateProfile(), createTask: async (input) => { createdInputs.push(input as CreatedTaskInput) return stubCreatedTask(202, input as CreatedTaskInput) }, createTaskEvent: async (taskId, eventType) => { taskEvents.push({ taskId: Number(taskId), eventType }) return null }, logInfo: () => null, logWarn: () => null, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-partial', }, ) assert.deepEqual( createdInputs.map((input) => [input.orderItemId, input.unitIndex]), [[21, 1]], ) assert.deepEqual( tasks.map((task) => task.id), [201, 202], ) assert.deepEqual(taskEvents, [{ taskId: 202, eventType: 'fulfillment_task_planned' }]) }) test('syncDeliveryTasksForOrder 数量增加时只补建缺失单元', async () => { const existingTask = stubCreatedTask(301, { orderId: 4, orderItemId: 30, unitIndex: 1, executorKey: 'affiliate_dash', }) const createdInputs: CreatedTaskInput[] = [] const tasks = await syncDeliveryTasksForOrderWithDeps( stubOrder({ id: 4, platform_order_id: 'quantity-increased' }), [ stubOrderItem({ id: 30, order_id: 4, sku_code: 'SKU-quantity', quantity: 2, item_snapshot_json: buildAffiliateSnapshot('AFF-A', '商品 A'), }), ], { listTasksByOrderId: async () => [existingTask], getFulfillmentProfileByKey: async () => buildAffiliateProfile(), createTask: async (input) => { createdInputs.push(input as CreatedTaskInput) return stubCreatedTask(302, input as CreatedTaskInput) }, createTaskEvent: async () => null, logInfo: () => null, logWarn: () => null, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-quantity', }, ) assert.deepEqual( createdInputs.map((input) => input.unitIndex), [2], ) assert.deepEqual( tasks.map((task) => task.unit_index), [1, 2], ) }) test('syncDeliveryTasksForOrder 规划失败时记录可定位的路由诊断', async () => { const warnings: Array<{ message: string; detail: Record }> = [] const tasks = await syncDeliveryTasksForOrderWithDeps( stubOrder({ id: 5, platform_order_id: 'planning-failed' }), [ stubOrderItem({ id: 40, order_id: 5, sku_code: 'SKU-broken', item_snapshot_json: { isConfigured: true, fulfillmentRoute: { selectedExecutorKey: 'affiliate_dash', reason: '规则指定 affiliate', }, affiliateDash: { skuName: '商品 A', }, } as JsonObject, }), ], { listTasksByOrderId: async () => [], getFulfillmentProfileByKey: async () => buildAffiliateProfile(), createTaskEvent: async () => null, logInfo: () => null, logWarn: (_scope, message, detail) => { warnings.push({ message: String(message), detail: detail as Record, }) return null }, }, ) assert.deepEqual(tasks, []) assert.equal(warnings.length, 1) assert.equal(warnings[0]?.message, '订单项已配置履约路由但无法生成任务计划') assert.equal(warnings[0]?.detail.orderItemId, 40) assert.deepEqual(warnings[0]?.detail.missingUnitIndexes, [1]) assert.deepEqual(warnings[0]?.detail.planningDiagnostic, { snapshotIsConfigured: true, selectedExecutorKey: 'affiliate_dash', routeReason: '规则指定 affiliate', candidateAvailability: [], sourceItemTitle: '', sourceSkuNick: '', cloudSkuId: 0, cloudSkuName: '', cloudSourceKeys: [], feifeiProductCode: '', affiliateDashSku: '', }) }) test('syncDeliveryTasksForOrder 并发创建冲突时复用已落库任务', async () => { const concurrentTask = stubCreatedTask(401, { orderId: 6, orderItemId: 50, unitIndex: 1, executorKey: 'affiliate_dash', }) let listCount = 0 let eventCount = 0 const tasks = await syncDeliveryTasksForOrderWithDeps( stubOrder({ id: 6, platform_order_id: 'concurrent-order' }), [ stubOrderItem({ id: 50, order_id: 6, sku_code: 'SKU-concurrent', item_snapshot_json: buildAffiliateSnapshot('AFF-A', '商品 A'), }), ], { listTasksByOrderId: async () => (listCount++ === 0 ? [] : [concurrentTask]), getFulfillmentProfileByKey: async () => buildAffiliateProfile(), createTask: async () => { throw new Error('duplicate key value violates unique constraint') }, createTaskEvent: async () => { eventCount += 1 return null }, logInfo: () => null, logWarn: () => null, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-concurrent', }, ) assert.deepEqual( tasks.map((task) => task.id), [401], ) assert.equal(eventCount, 0) }) function buildAffiliateSnapshot(sku: string, productName: string): JsonObject { return { isConfigured: true, fulfillmentRoute: { selectedExecutorKey: 'affiliate_dash', }, affiliateDash: { sku, skuName: productName, matchMode: 'affiliate_dash_sku_mapping', }, } } function buildAffiliateProfile() { return { id: 11, profile_key: 'affiliate_dash', profile_name: 'affiliate-dash 履约', executor_key: 'affiliate_dash', } }