后端补充自动发货服务测试
This commit is contained in:
@@ -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 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user