436 lines
13 KiB
TypeScript
436 lines
13 KiB
TypeScript
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<OrderRow> & Pick<OrderRow, 'id' | 'platform_order_id'>,
|
|
): 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<OrderItemRow> & Pick<OrderItemRow, 'id' | 'order_id' | 'sku_code'>,
|
|
): 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 多账号候选时不提前固定 cloudtentacles 账号', 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: 'SKU-1',
|
|
sku_name: '荣耀勋章礼包(30个)',
|
|
quantity: 1,
|
|
item_snapshot_json: {
|
|
cloudtentacles: {
|
|
cloudSourceKeys: ['account-a', 'account-b'],
|
|
resolvedSourceKey: 'account-a',
|
|
cloudSkuId: 74,
|
|
cloudSkuName: '荣耀勋章礼包(30个)',
|
|
deliveryItems: [
|
|
{
|
|
cloudSkuId: 74,
|
|
cloudSkuName: '荣耀勋章礼包(30个)',
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
} as JsonObject,
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => [],
|
|
getFulfillmentProfileByKey: async () => ({
|
|
id: 9,
|
|
profile_key: 'kuaishou_ct_assisted',
|
|
profile_name: 'kuaishou-lewan 履约',
|
|
executor_key: 'kuaishou_ct_assisted',
|
|
}),
|
|
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,
|
|
},
|
|
)
|
|
|
|
const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject
|
|
const fulfillment = context.kuaishouCloudFulfillment as JsonObject
|
|
const binding = fulfillment.binding as JsonObject
|
|
assert.deepEqual(binding.cloudSourceKeys, ['account-a', 'account-b'])
|
|
assert.equal(binding.resolvedSourceKey, '')
|
|
})
|
|
|
|
test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles 账号', async () => {
|
|
const createdInputs: CreatedTaskInput[] = []
|
|
|
|
await syncDeliveryTasksForOrderWithDeps(
|
|
stubOrder({
|
|
id: 2,
|
|
pay_status: 'pending_payment',
|
|
platform_order_id: '2614900602069170',
|
|
}),
|
|
[
|
|
stubOrderItem({
|
|
id: 11,
|
|
order_id: 2,
|
|
sku_code: 'SKU-1',
|
|
sku_name: '荣耀勋章礼包(30个)',
|
|
quantity: 1,
|
|
item_snapshot_json: {
|
|
cloudtentacles: {
|
|
cloudSourceKeys: ['account-a'],
|
|
cloudSkuId: 74,
|
|
cloudSkuName: '荣耀勋章礼包(30个)',
|
|
},
|
|
} as JsonObject,
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => [],
|
|
getFulfillmentProfileByKey: async () => ({
|
|
id: 9,
|
|
profile_key: 'kuaishou_ct_assisted',
|
|
profile_name: 'kuaishou-lewan 履约',
|
|
executor_key: 'kuaishou_ct_assisted',
|
|
}),
|
|
createTask: async (input) => {
|
|
createdInputs.push(input as CreatedTaskInput)
|
|
return stubCreatedTask(101, input as CreatedTaskInput)
|
|
},
|
|
nowIso: () => '2026-05-29T09:05:46.658Z',
|
|
randomId: () => 'DT-test',
|
|
createTaskEvent: async () => null,
|
|
},
|
|
)
|
|
|
|
const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject
|
|
const fulfillment = context.kuaishouCloudFulfillment as JsonObject
|
|
const binding = fulfillment.binding as JsonObject
|
|
assert.deepEqual(binding.cloudSourceKeys, ['account-a'])
|
|
assert.equal(binding.resolvedSourceKey, 'account-a')
|
|
})
|
|
|
|
test('syncDeliveryTasksForOrder 已有部分任务时按订单项补建缺失任务', async () => {
|
|
const existingTask = stubCreatedTask(201, {
|
|
orderId: 3,
|
|
orderItemId: 20,
|
|
unitIndex: 1,
|
|
executorKey: 'kuaishou_ct_assisted',
|
|
})
|
|
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: buildCloudSnapshot(74, '商品 A'),
|
|
}),
|
|
stubOrderItem({
|
|
id: 21,
|
|
order_id: 3,
|
|
sku_code: 'SKU-missing',
|
|
item_snapshot_json: buildCloudSnapshot(75, '商品 B'),
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => [existingTask],
|
|
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
|
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: 'kuaishou_ct_assisted',
|
|
})
|
|
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: buildCloudSnapshot(74, '商品 A'),
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => [existingTask],
|
|
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
|
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<string, unknown> }> = []
|
|
|
|
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: 'kuaishou_ct_assisted',
|
|
reason: '规则指定云履约',
|
|
},
|
|
cloudtentacles: {
|
|
cloudSkuId: 74,
|
|
cloudSkuName: '商品 A',
|
|
cloudSourceKeys: [],
|
|
},
|
|
} as JsonObject,
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => [],
|
|
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
|
createTaskEvent: async () => null,
|
|
logInfo: () => null,
|
|
logWarn: (_scope, message, detail) => {
|
|
warnings.push({
|
|
message: String(message),
|
|
detail: detail as Record<string, unknown>,
|
|
})
|
|
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: 'kuaishou_ct_assisted',
|
|
routeReason: '规则指定云履约',
|
|
candidateAvailability: [],
|
|
sourceItemTitle: '',
|
|
sourceSkuNick: '',
|
|
cloudSkuId: 74,
|
|
cloudSkuName: '商品 A',
|
|
cloudSourceKeys: [],
|
|
feifeiProductCode: '',
|
|
affiliateDashSku: '',
|
|
})
|
|
})
|
|
|
|
test('syncDeliveryTasksForOrder 并发创建冲突时复用已落库任务', async () => {
|
|
const concurrentTask = stubCreatedTask(401, {
|
|
orderId: 6,
|
|
orderItemId: 50,
|
|
unitIndex: 1,
|
|
executorKey: 'kuaishou_ct_assisted',
|
|
})
|
|
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: buildCloudSnapshot(74, '商品 A'),
|
|
}),
|
|
],
|
|
{
|
|
listTasksByOrderId: async () => (listCount++ === 0 ? [] : [concurrentTask]),
|
|
getFulfillmentProfileByKey: async () => buildCloudProfile(),
|
|
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 buildCloudSnapshot(cloudSkuId: number, cloudSkuName: string): JsonObject {
|
|
return {
|
|
isConfigured: true,
|
|
fulfillmentRoute: {
|
|
selectedExecutorKey: 'kuaishou_ct_assisted',
|
|
},
|
|
cloudtentacles: {
|
|
cloudSourceKeys: ['account-a'],
|
|
cloudSkuId,
|
|
cloudSkuName,
|
|
},
|
|
}
|
|
}
|
|
|
|
function buildCloudProfile() {
|
|
return {
|
|
id: 9,
|
|
profile_key: 'kuaishou_ct_assisted',
|
|
profile_name: 'kuaishou-lewan 履约',
|
|
executor_key: 'kuaishou_ct_assisted',
|
|
}
|
|
}
|