后端补充履约任务服务测试
This commit is contained in:
@@ -10,7 +10,32 @@ import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
|
||||
export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
const existingTasks = await listTasksByOrderId(order.id)
|
||||
return syncDeliveryTasksForOrderWithDeps(order, orderItems)
|
||||
}
|
||||
|
||||
export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps = {}) {
|
||||
const {
|
||||
createTask: createDeliveryTask = createTask,
|
||||
listTasksByOrderId: listTasks = listTasksByOrderId,
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
listFulfillmentProfileRequirements: listProfileRequirements = listFulfillmentProfileRequirements,
|
||||
resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding,
|
||||
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
randomId: createRandomId = randomId,
|
||||
} = deps
|
||||
|
||||
const runtimeDeps = {
|
||||
updateTask: updateDeliveryTask,
|
||||
reserveInventoryForTask: reserveInventory,
|
||||
createTaskClaimToken: createClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview,
|
||||
nowIso: getNowIso,
|
||||
}
|
||||
|
||||
const existingTasks = await listTasks(order.id)
|
||||
|
||||
if (existingTasks.length > 0) {
|
||||
if (order.pay_status !== 'paid') {
|
||||
@@ -22,13 +47,13 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
...task,
|
||||
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
|
||||
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
|
||||
})))
|
||||
}, runtimeDeps)))
|
||||
}
|
||||
|
||||
const tasks = []
|
||||
|
||||
for (const item of orderItems) {
|
||||
const binding = await resolveFulfillmentBinding({
|
||||
const binding = await resolveBinding({
|
||||
skuCode: item.sku_code,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
@@ -39,7 +64,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
continue
|
||||
}
|
||||
const profile = binding
|
||||
const requirements = await listFulfillmentProfileRequirements(profile.profile_id || profile.id)
|
||||
const requirements = await listProfileRequirements(profile.profile_id || profile.id)
|
||||
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
@@ -48,12 +73,12 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
: {}
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = nowIso()
|
||||
const createdAt = getNowIso()
|
||||
const initialStatus = order.pay_status === 'paid'
|
||||
? resolvePaidTaskStatus(profile)
|
||||
: 'pending_payment'
|
||||
|
||||
const task = await createTask({
|
||||
const task = await createDeliveryTask({
|
||||
orderId: order.id,
|
||||
orderItemId: item.id,
|
||||
unitIndex: index + 1,
|
||||
@@ -62,7 +87,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
shopId: order.shop_id,
|
||||
shopName: order.shop_name,
|
||||
platformOrderId: order.platform_order_id,
|
||||
taskNo: randomId('DT'),
|
||||
taskNo: createRandomId('DT'),
|
||||
profileId: Number(profile.profile_id || profile.id),
|
||||
executorKey: String(profile.executor_key || 'manual_dispatch'),
|
||||
taskStatus: initialStatus,
|
||||
@@ -179,11 +204,19 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
return tasks
|
||||
}
|
||||
|
||||
return Promise.all(tasks.map((task) => preparePaidTask(task)))
|
||||
return Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
|
||||
}
|
||||
|
||||
async function preparePaidTask(task) {
|
||||
const now = nowIso()
|
||||
async function preparePaidTask(task, deps = {}) {
|
||||
const {
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
} = deps
|
||||
|
||||
const now = getNowIso()
|
||||
const taskContext = parseTaskContext(task)
|
||||
const primaryRequirement = taskContext.primaryRequirement || null
|
||||
const inventorySkuCode = String(taskContext.inventorySkuCode || task.skuCode || '').trim()
|
||||
@@ -202,8 +235,8 @@ async function preparePaidTask(task) {
|
||||
'failed',
|
||||
].includes(String(task.task_status || '').trim())) {
|
||||
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
return updateTask(task.id, {
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
return updateDeliveryTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
@@ -214,9 +247,9 @@ async function preparePaidTask(task) {
|
||||
return task
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
|
||||
return updateTask(task.id, {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
claim_token: claimToken.token,
|
||||
@@ -229,14 +262,14 @@ async function preparePaidTask(task) {
|
||||
|
||||
if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
|
||||
const lastError = task.last_error || '当前任务需要人工履约处理'
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'not_required',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyTaskAutoManualReview({
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'manual_dispatch_profile',
|
||||
@@ -246,12 +279,12 @@ async function preparePaidTask(task) {
|
||||
|
||||
if (!inventorySkuCode) {
|
||||
const lastError = '未匹配到 SKU,无法为任务分配库存凭据'
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyTaskAutoManualReview({
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'missing_sku',
|
||||
@@ -260,7 +293,7 @@ async function preparePaidTask(task) {
|
||||
}
|
||||
|
||||
if (task.primary_inventory_item_id && task.primary_claim_token_id) {
|
||||
return updateTask(task.id, {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
@@ -269,12 +302,12 @@ async function preparePaidTask(task) {
|
||||
|
||||
if (!primaryRequirement?.credentialType) {
|
||||
const lastError = '履约档案未配置库存要求,无法自动分配库存'
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyTaskAutoManualReview({
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'missing_inventory_requirement',
|
||||
@@ -282,7 +315,7 @@ async function preparePaidTask(task) {
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
const reserved = await reserveInventoryForTask({
|
||||
const reserved = await reserveInventory({
|
||||
skuCode: inventorySkuCode,
|
||||
taskId: task.id,
|
||||
credentialType: primaryRequirement.credentialType,
|
||||
@@ -290,7 +323,7 @@ async function preparePaidTask(task) {
|
||||
})
|
||||
|
||||
if (!reserved) {
|
||||
return updateTask(task.id, {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'waiting_inventory',
|
||||
inventory_status: 'pending',
|
||||
last_error: '库存不足,等待可用库存凭据',
|
||||
@@ -298,9 +331,9 @@ async function preparePaidTask(task) {
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
|
||||
return updateTask(task.id, {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
inventory_status: 'reserved',
|
||||
claim_token: claimToken.token,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js'
|
||||
|
||||
const paidOrder = {
|
||||
id: 10,
|
||||
provider: 'agiso',
|
||||
platform: 'xianyu',
|
||||
shop_id: 'shop-1',
|
||||
shop_name: '测试店铺',
|
||||
platform_order_id: 'P10001',
|
||||
pay_status: 'paid',
|
||||
}
|
||||
|
||||
const orderItems = [
|
||||
{
|
||||
id: 20,
|
||||
sku_code: 'dnf-cdk-a',
|
||||
sku_name: 'DNF CDK A',
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
|
||||
function createTaskFixture(patch = {}) {
|
||||
return {
|
||||
id: 30,
|
||||
order_item_id: 20,
|
||||
task_status: 'paid',
|
||||
executor_key: 'tencent_claim_assisted',
|
||||
requires_claim: true,
|
||||
claim_token: '',
|
||||
primary_claim_token_id: null,
|
||||
primary_inventory_item_id: null,
|
||||
last_error: '',
|
||||
context_json: JSON.stringify({
|
||||
inventorySkuCode: 'dnf-cdk-a',
|
||||
primaryRequirement: {
|
||||
roleKey: 'primary_code',
|
||||
credentialType: 'tencent_code',
|
||||
},
|
||||
}),
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
|
||||
test('syncDeliveryTasksForOrderWithDeps moves paid claim task to waiting inventory when reservation fails', async () => {
|
||||
const updates = []
|
||||
const reserveCalls = []
|
||||
|
||||
const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, {
|
||||
listTasksByOrderId: async () => [createTaskFixture()],
|
||||
reserveInventoryForTask: async (payload) => {
|
||||
reserveCalls.push(payload)
|
||||
return null
|
||||
},
|
||||
updateTask: async (taskId, patch) => {
|
||||
updates.push({ taskId, patch })
|
||||
return { id: taskId, ...patch }
|
||||
},
|
||||
nowIso: () => '2026-04-14T12:00:00.000Z',
|
||||
})
|
||||
|
||||
assert.deepEqual(reserveCalls, [
|
||||
{
|
||||
skuCode: 'dnf-cdk-a',
|
||||
taskId: 30,
|
||||
credentialType: 'tencent_code',
|
||||
roleKey: 'primary_code',
|
||||
},
|
||||
])
|
||||
assert.deepEqual(updates, [
|
||||
{
|
||||
taskId: 30,
|
||||
patch: {
|
||||
task_status: 'waiting_inventory',
|
||||
inventory_status: 'pending',
|
||||
last_error: '库存不足,等待可用库存凭据',
|
||||
updated_at: '2026-04-14T12:00:00.000Z',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(result[0]?.task_status, 'waiting_inventory')
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrderWithDeps sends manual dispatch tasks to manual review', async () => {
|
||||
const notifications = []
|
||||
const updates = []
|
||||
|
||||
const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, {
|
||||
listTasksByOrderId: async () => [createTaskFixture({
|
||||
executor_key: 'manual_dispatch',
|
||||
requires_claim: false,
|
||||
context_json: '{}',
|
||||
})],
|
||||
updateTask: async (taskId, patch) => {
|
||||
updates.push({ taskId, patch })
|
||||
return { id: taskId, ...patch }
|
||||
},
|
||||
notifyTaskAutoManualReview: async (payload) => {
|
||||
notifications.push(payload)
|
||||
return { ok: true }
|
||||
},
|
||||
nowIso: () => '2026-04-14T12:01:00.000Z',
|
||||
})
|
||||
|
||||
assert.equal(result[0]?.task_status, 'manual_review')
|
||||
assert.equal(updates[0]?.patch.last_error, '当前任务需要人工履约处理')
|
||||
assert.deepEqual(
|
||||
notifications.map((item) => ({
|
||||
reason: item.reason,
|
||||
source: item.source,
|
||||
taskStatus: item.task.task_status,
|
||||
})),
|
||||
[
|
||||
{
|
||||
reason: '当前任务需要人工履约处理',
|
||||
source: 'manual_dispatch_profile',
|
||||
taskStatus: 'manual_review',
|
||||
},
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrderWithDeps prepares kuaishou cloud task with claim token', async () => {
|
||||
const claimToken = {
|
||||
token: 'claim-token',
|
||||
expired_at: '2026-04-15T12:00:00.000Z',
|
||||
}
|
||||
const updates = []
|
||||
|
||||
const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, {
|
||||
listTasksByOrderId: async () => [createTaskFixture({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
task_status: 'paid',
|
||||
requires_claim: true,
|
||||
context_json: '{}',
|
||||
})],
|
||||
createTaskClaimToken: async (taskId) => ({ ...claimToken, taskId }),
|
||||
updateTask: async (taskId, patch) => {
|
||||
updates.push({ taskId, patch })
|
||||
return { id: taskId, ...patch }
|
||||
},
|
||||
nowIso: () => '2026-04-14T12:02:00.000Z',
|
||||
})
|
||||
|
||||
assert.equal(result[0]?.task_status, 'pending_binding_prepare')
|
||||
assert.deepEqual(updates, [
|
||||
{
|
||||
taskId: 30,
|
||||
patch: {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
claim_token: 'claim-token',
|
||||
claim_expires_at: '2026-04-15T12:00:00.000Z',
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '领取链接已生成,等待客户提交核销码',
|
||||
updated_at: '2026-04-14T12:02:00.000Z',
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -288,14 +288,23 @@
|
||||
- `npm run typecheck`
|
||||
- `npm run build`
|
||||
- `npm test` 共 125 个用例通过
|
||||
37. delivery task 服务迁移前补直接测试:
|
||||
- 新增 `src/services/order/delivery-task-service.test.js`
|
||||
- 新增 `syncDeliveryTasksForOrderWithDeps`,便于隔离 repository / 库存 / claim token / 通知依赖
|
||||
- 覆盖库存不足进入 `waiting_inventory`、人工履约进入 `manual_review`、快手云任务生成 claim token 三个关键分支
|
||||
38. Docker 内验证通过:
|
||||
- `src/services/order/delivery-task-service.test.js` 共 3 个用例通过
|
||||
- `npm run typecheck`
|
||||
- `npm run build`
|
||||
- `npm test` 共 128 个用例通过
|
||||
|
||||
## 下一步建议
|
||||
|
||||
第一批继续推进时,建议按这个顺序:
|
||||
|
||||
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
||||
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货、delivery task
|
||||
3. 为 delivery task 补直接单测后再迁移,避免只依赖订单链路间接覆盖
|
||||
2. 迁移 delivery task 服务到 `.ts`
|
||||
3. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货
|
||||
|
||||
## 执行原则
|
||||
|
||||
|
||||
Reference in New Issue
Block a user