后端补充自动发货服务测试

This commit is contained in:
yml
2026-05-21 14:42:19 +08:00
parent 0d33778d4b
commit 861b7e0034
3 changed files with 220 additions and 16 deletions
@@ -28,6 +28,18 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
task, task,
trigger = 'task_delivered', trigger = 'task_delivered',
} = {}) { } = {}) {
return ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ order, task, trigger })
}
export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({
order,
task,
trigger = 'task_delivered',
} = {}, deps = {}) {
const listOrderTasks = deps.listTasksByOrderId || listTasksByOrderId
const sendRequest = deps.fetch || fetch
const confirmShipped = deps.confirmAgisoXianyuAutoDeliveryShipped || confirmAgisoXianyuAutoDeliveryShipped
if (!isAgisoXianyuOrder(order) || !task?.id) { if (!isAgisoXianyuOrder(order) || !task?.id) {
return { sent: false, skipped: true, reason: 'not_supported', task } return { sent: false, skipped: true, reason: 'not_supported', task }
} }
@@ -36,7 +48,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
return { sent: false, skipped: true, reason: 'task_not_delivered', task } return { sent: false, skipped: true, reason: 'task_not_delivered', task }
} }
const orderTasks = await listTasksByOrderId(order.id) const orderTasks = await listOrderTasks(order.id)
if (hasAgisoAutoDeliverySucceeded(orderTasks)) { if (hasAgisoAutoDeliverySucceeded(orderTasks)) {
return { sent: false, skipped: true, reason: 'already_sent', task } return { sent: false, skipped: true, reason: 'already_sent', task }
} }
@@ -64,7 +76,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
trigger, trigger,
reason: 'auto_delivery_disabled', reason: 'auto_delivery_disabled',
order, order,
}) }, deps)
} }
if (!config.endpoint || !config.accessToken || !config.appSecret) { if (!config.endpoint || !config.accessToken || !config.appSecret) {
@@ -78,7 +90,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
hasAccessToken: Boolean(config.accessToken), hasAccessToken: Boolean(config.accessToken),
hasAppSecret: Boolean(config.appSecret), hasAppSecret: Boolean(config.appSecret),
}, },
}) }, deps)
} }
const requestHeaders = buildRequestHeaders({ const requestHeaders = buildRequestHeaders({
@@ -100,7 +112,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
}) })
try { try {
const response = await fetch(config.endpoint, { const response = await sendRequest(config.endpoint, {
method: 'POST', method: 'POST',
headers: requestHeaders, headers: requestHeaders,
body: new URLSearchParams(requestBody).toString(), body: new URLSearchParams(requestBody).toString(),
@@ -113,7 +125,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
const requestId = String(parsed?.RequestId || '').trim() const requestId = String(parsed?.RequestId || '').trim()
// 发货接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~ // 发货接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~
const confirmResult = await confirmAgisoXianyuAutoDeliveryShipped({ const confirmResult = await confirmShipped({
shopId: order.shop_id, shopId: order.shop_id,
platformOrderId: order.platform_order_id, platformOrderId: order.platform_order_id,
requestId, requestId,
@@ -146,7 +158,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
confirmOrderStatus: confirmResult.orderStatus, confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime, confirmShipTime: confirmResult.shipTime,
}, },
}) }, deps)
} }
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', { logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', {
@@ -173,10 +185,10 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
confirmOrderStatus: confirmResult.orderStatus, confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime, confirmShipTime: confirmResult.shipTime,
}, },
}) }, deps)
// 发货确认成功后,发送自定义消息通知 meow~ // 发货确认成功后,发送自定义消息通知 meow~
await sendAutoDeliveryMessage({ order, task }) await sendAutoDeliveryMessage({ order, task }, deps)
return result return result
} }
@@ -201,7 +213,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
responseStatus: response.status, responseStatus: response.status,
response: parsed, response: parsed,
errorMessage, errorMessage,
}) }, deps)
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼自动发货失败') const errorMessage = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼自动发货失败')
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货异常', { logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货异常', {
@@ -221,7 +233,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
responseStatus: 0, responseStatus: 0,
response: {}, response: {},
errorMessage, errorMessage,
}) }, deps)
} }
} }
@@ -333,8 +345,11 @@ async function persistAgisoAutoDeliveryResult(task, {
response = {}, response = {},
errorMessage = '', errorMessage = '',
detail = {}, detail = {},
} = {}) { } = {}, deps = {}) {
const now = nowIso() const getNowIso = deps.nowIso || nowIso
const patchTask = deps.updateTask || updateTask
const insertTaskEvent = deps.createTaskEvent || createTaskEvent
const now = getNowIso()
const currentContext = parseTaskContext(task) const currentContext = parseTaskContext(task)
const nextContext = { const nextContext = {
...currentContext, ...currentContext,
@@ -350,13 +365,13 @@ async function persistAgisoAutoDeliveryResult(task, {
...detail, ...detail,
}, },
} }
const updatedTask = await updateTask(task.id, { const updatedTask = await patchTask(task.id, {
context_json: JSON.stringify(nextContext), context_json: JSON.stringify(nextContext),
updated_at: now, updated_at: now,
}) })
if (status === 'success' || status === 'failed' || status === 'skipped') { if (status === 'success' || status === 'failed' || status === 'skipped') {
await createTaskEvent(task.id, `agiso_auto_delivery_${status}`, { await insertTaskEvent(task.id, `agiso_auto_delivery_${status}`, {
trigger: String(trigger || '').trim(), trigger: String(trigger || '').trim(),
reason: String(reason || '').trim(), reason: String(reason || '').trim(),
platformOrderId: String(order?.platform_order_id || '').trim(), platformOrderId: String(order?.platform_order_id || '').trim(),
@@ -471,9 +486,11 @@ async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrde
/** /**
* 自动发货成功后发送消息通知 meow~ * 自动发货成功后发送消息通知 meow~
*/ */
async function sendAutoDeliveryMessage({ order, task } = {}) { async function sendAutoDeliveryMessage({ order, task } = {}, deps = {}) {
try { try {
const result = await ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task }) const deliverMessage = deps.ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask
|| ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask
const result = await deliverMessage({ order, task })
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知结果', { logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知结果', {
orderId: order?.id, orderId: order?.id,
taskId: task?.id, taskId: task?.id,
@@ -1,14 +1,36 @@
import test from 'node:test' import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { runtimeConfig } from '../../../../config/runtime.js'
import { import {
hasAgisoAutoDeliverySucceeded, hasAgisoAutoDeliverySucceeded,
ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps,
isOrderReadyForAgisoAutoDelivery, isOrderReadyForAgisoAutoDelivery,
isAgisoAutoDeliverySuccess, isAgisoAutoDeliverySuccess,
resolveAgisoAutoDeliveryEndpoint, resolveAgisoAutoDeliveryEndpoint,
resolveAgisoAutoDeliveryErrorMessage, resolveAgisoAutoDeliveryErrorMessage,
} from './auto-delivery-service.js' } from './auto-delivery-service.js'
const agisoOrder = {
id: 101,
provider: 'agiso',
platform: 'xianyu',
shop_id: 'shop-auto-delivery-test',
shop_name: '自动发货测试店',
platform_order_id: 'P-AUTO-10001',
}
function createDeliveredTask(patch = {}) {
return {
id: 201,
order_id: agisoOrder.id,
task_status: 'completed',
delivery_status: 'delivered',
context_json: '{}',
...patch,
}
}
test('isOrderReadyForAgisoAutoDelivery requires every task to be delivered', () => { test('isOrderReadyForAgisoAutoDelivery requires every task to be delivered', () => {
assert.equal(isOrderReadyForAgisoAutoDelivery([]), false) assert.equal(isOrderReadyForAgisoAutoDelivery([]), false)
assert.equal(isOrderReadyForAgisoAutoDelivery([ assert.equal(isOrderReadyForAgisoAutoDelivery([
@@ -83,3 +105,160 @@ test('resolveAgisoAutoDeliveryEndpoint falls back to DummySend', () => {
'https://gw-api.agiso.com/aldsIdle/Order/DummySend', 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
) )
}) })
test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps skips while other order tasks are not delivered', async () => {
const calls = {
update: 0,
fetch: 0,
}
const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({
order: agisoOrder,
task: createDeliveredTask(),
}, {
listTasksByOrderId: async () => [
createDeliveredTask(),
createDeliveredTask({ id: 202, delivery_status: 'processing' }),
],
updateTask: async () => {
calls.update += 1
return null
},
fetch: async () => {
calls.fetch += 1
return { status: 200, text: async () => '{}' }
},
})
assert.equal(result.sent, false)
assert.equal(result.skipped, true)
assert.equal(result.reason, 'waiting_other_tasks')
assert.equal(calls.update, 0)
assert.equal(calls.fetch, 0)
})
test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps persists missing config as skipped', async () => {
const originalAppSecret = runtimeConfig.platforms.agiso.appSecret
const originalShops = runtimeConfig.platforms.agiso.messaging.shops
const updates = []
const events = []
runtimeConfig.platforms.agiso.appSecret = ''
runtimeConfig.platforms.agiso.messaging.shops = {}
try {
const task = createDeliveredTask({ context_json: JSON.stringify({ existing: true }) })
const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({
order: {
...agisoOrder,
shop_id: 'missing-auto-delivery-config-shop',
},
task,
trigger: 'unit_test',
}, {
listTasksByOrderId: async () => [task],
updateTask: async (taskId, patch) => {
updates.push({ taskId, patch })
return { ...task, ...patch }
},
createTaskEvent: async (taskId, eventType, payload, createdAt) => {
events.push({ taskId, eventType, payload, createdAt })
return { id: 1 }
},
fetch: async () => {
throw new Error('fetch should not be called without config')
},
nowIso: () => '2026-05-21T12:00:00.000Z',
})
assert.equal(result.sent, false)
assert.equal(result.skipped, true)
assert.equal(result.reason, 'missing_config')
assert.equal(updates.length, 1)
assert.equal(events[0]?.eventType, 'agiso_auto_delivery_skipped')
const context = JSON.parse(updates[0].patch.context_json)
assert.equal(context.existing, true)
assert.equal(context.agisoAutoDelivery.status, 'skipped')
assert.equal(context.agisoAutoDelivery.reason, 'missing_config')
assert.deepEqual({
hasEndpoint: context.agisoAutoDelivery.hasEndpoint,
hasAccessToken: context.agisoAutoDelivery.hasAccessToken,
hasAppSecret: context.agisoAutoDelivery.hasAppSecret,
}, {
hasEndpoint: true,
hasAccessToken: false,
hasAppSecret: false,
})
} finally {
runtimeConfig.platforms.agiso.appSecret = originalAppSecret
runtimeConfig.platforms.agiso.messaging.shops = originalShops
}
})
test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps fails when accepted delivery is not confirmed as shipped', async () => {
const originalAppSecret = runtimeConfig.platforms.agiso.appSecret
const originalShops = runtimeConfig.platforms.agiso.messaging.shops
const updates = []
const events = []
const messages = []
runtimeConfig.platforms.agiso.appSecret = 'runtime-secret'
runtimeConfig.platforms.agiso.messaging.shops = {
[agisoOrder.shop_id]: {
accessToken: 'access-token',
appSecret: 'shop-secret',
apiVersion: '1',
},
}
try {
const task = createDeliveredTask()
const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({
order: agisoOrder,
task,
trigger: 'unit_test',
}, {
listTasksByOrderId: async () => [task],
fetch: async () => ({
status: 200,
text: async () => JSON.stringify({ IsSuccess: true, RequestId: 'req-1' }),
}),
confirmAgisoXianyuAutoDeliveryShipped: async (input) => ({
shipped: false,
orderStatus: 2,
shipTime: 0,
reason: `not shipped: ${input.requestId}`,
}),
updateTask: async (taskId, patch) => {
updates.push({ taskId, patch })
return { ...task, ...patch }
},
createTaskEvent: async (taskId, eventType, payload, createdAt) => {
events.push({ taskId, eventType, payload, createdAt })
return { id: 2 }
},
ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask: async (payload) => {
messages.push(payload)
return { sent: true }
},
nowIso: () => '2026-05-21T12:01:00.000Z',
})
assert.equal(result.sent, false)
assert.equal(result.skipped, false)
assert.equal(result.reason, 'delivery_not_confirmed')
assert.equal(messages.length, 0)
assert.equal(events[0]?.eventType, 'agiso_auto_delivery_failed')
const context = JSON.parse(updates[0].patch.context_json)
assert.equal(context.agisoAutoDelivery.status, 'failed')
assert.equal(context.agisoAutoDelivery.reason, 'delivery_not_confirmed')
assert.equal(context.agisoAutoDelivery.requestId, 'req-1')
assert.equal(context.agisoAutoDelivery.confirmOrderStatus, 2)
assert.match(context.agisoAutoDelivery.errorMessage, /订单仍未进入已发货/)
} finally {
runtimeConfig.platforms.agiso.appSecret = originalAppSecret
runtimeConfig.platforms.agiso.messaging.shops = originalShops
}
})
@@ -312,6 +312,14 @@
- `npm run typecheck` - `npm run typecheck`
- `npm run build` - `npm run build`
- `npm test` 共 128 个用例通过 - `npm test` 共 128 个用例通过
45. Agiso 自动发货服务迁移前补直接测试:
- 新增 `ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps`,便于隔离 task repository、HTTP 请求、发货确认与消息通知依赖
- 覆盖订单下仍有任务未交付时跳过、配置缺失时记录 `skipped`、接口受理但未确认发货时记录 `failed` 三个关键分支
46. Docker 内验证通过:
- `src/services/platforms/agiso/xianyu/auto-delivery-service.test.js` 共 9 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 131 个用例通过
## 下一步建议 ## 下一步建议