后端补充Webhook编排测试

This commit is contained in:
yml
2026-05-21 15:04:39 +08:00
parent 1a07431887
commit 26739235e4
3 changed files with 237 additions and 9 deletions
@@ -80,6 +80,19 @@ export function buildWebhookEventInput(requestLike, parsed) {
}
async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' } = {}) {
return executeAgisoTradeWebhookWithDeps(parsed, webhookEventId, { requestId })
}
export async function executeAgisoTradeWebhookWithDeps(
parsed,
webhookEventId,
{ requestId = '' } = {},
deps = {},
) {
const updateEvent = deps.updateWebhookEvent || updateWebhookEvent
const hasConfiguredItems = deps.hasConfiguredOrderItems || hasConfiguredOrderItems
const upsertOrder = deps.upsertOrderFromWebhook || upsertOrderFromWebhook
try {
if (!parsed.signatureValid) {
throw createHttpError('验签失败', {
@@ -97,7 +110,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
const ignoreReason = resolveAgisoTradeIgnoreReason(parsed)
if (ignoreReason) {
await updateWebhookEvent(webhookEventId, {
await updateEvent(webhookEventId, {
processed: true,
process_error: `ignored_${ignoreReason}`,
related_order_id: null,
@@ -137,7 +150,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
}
}
const shouldProcess = await hasConfiguredOrderItems({
const shouldProcess = await hasConfiguredItems({
provider: parsed.provider,
platform: parsed.platform,
shopId: parsed.shopId,
@@ -146,7 +159,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
})
if (!shouldProcess) {
await updateWebhookEvent(webhookEventId, {
await updateEvent(webhookEventId, {
processed: true,
process_error: 'ignored_unconfigured_product',
related_order_id: null,
@@ -187,9 +200,9 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
const enriched = await enrichTradeBeforeUpsert(parsed, {
requestId,
webhookEventId,
})
const result = await upsertOrderFromWebhook(enriched.parsed)
await updateWebhookEvent(webhookEventId, {
}, deps)
const result = await upsertOrder(enriched.parsed)
await updateEvent(webhookEventId, {
processed: true,
process_error: result.ignored ? 'ignored_unconfigured_product' : '',
related_order_id: result.order?.id || null,
@@ -239,7 +252,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
})),
}
} catch (error) {
await updateWebhookEvent(webhookEventId, {
await updateEvent(webhookEventId, {
processed: false,
process_error: error instanceof Error ? error.message : String(error || ''),
related_order_id: null,
@@ -267,7 +280,9 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
}
}
async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId = null } = {}) {
async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId = null } = {}, deps = {}) {
const enrichOrder = deps.enrichAgisoXianyuTradeOrder || enrichAgisoXianyuTradeOrder
if (parsed.provider !== 'agiso' || parsed.platform !== 'xianyu') {
return { parsed, enriched: false, reason: 'not_supported' }
}
@@ -276,7 +291,7 @@ async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId
return { parsed, enriched: false, reason: 'enough_fields_present' }
}
const detailResult = await enrichAgisoXianyuTradeOrder(parsed, { requestId })
const detailResult = await enrichOrder(parsed, { requestId })
const nextParsed = detailResult?.parsed || parsed
logWebhook('[webhook-service/agiso]', 'Agiso webhook 订单补查结束', {
@@ -5,12 +5,46 @@ import crypto from 'node:crypto'
import { runtimeConfig } from '../../config/runtime.js'
import {
buildWebhookEventInput,
executeAgisoTradeWebhookWithDeps,
parseAgisoTradeRequest,
resolveAgisoTradeIgnoreReason,
shouldEnrichAgisoXianyuTrade,
verifyAgisoSignatureWithSecret,
} from './webhook-service.js'
function createParsedTrade(patch = {}) {
return {
provider: 'agiso',
platform: 'xianyu',
platformRaw: 'xianyu',
eventType: 'payment_success',
eventKey: 'agiso:xianyu:shop-9:4502259793206016214:payment_success:1713097840',
signatureValid: true,
platformOrderId: '4502259793206016214',
shopId: 'shop-9',
shopName: '咸鱼店铺',
shopIdAliases: ['shop-9'],
orderStatus: 'paid',
payStatus: 'paid',
buyerId: 'buyer-1',
buyerName: '测试买家',
receiverContact: '',
totalAmount: 2550,
currency: 'CNY',
paidAt: '2026-04-14T12:30:40.000Z',
rawPayload: {},
items: [
{
skuCode: 'dnf-cdk-a',
skuName: 'DNF礼包',
quantity: 1,
spec: { title: 'DNF礼包' },
},
],
...patch,
}
}
test('parseAgisoTradeRequest maps Agiso payment_success webhook into stable order fields', () => {
const rawJson = '{"biz_order_id":4502259793206016214,"seller_id":"shop-9","seller_nick":"咸鱼店铺","buyer_id":"buyer-1","buyer_name":"测试买家","pay_time":"2026-04-14 20:30:40","total_fee":"25.50","currency":"CNY","items":[{"item_id":978102431355,"goods_name":"DNF礼包","quantity":2,"sku":"dnf-cdk-a|商品名称:DNF礼包"}]}'
const timestamp = '1713097840'
@@ -180,3 +214,174 @@ test('buildWebhookEventInput preserves raw request payload for later replay', ()
assert.deepEqual(JSON.parse(input.bodyJson), { json: '{"biz_order_id":"4502259793206016214"}' })
assert.match(input.createdAt, /^\d{4}-\d{2}-\d{2}T/)
})
test('executeAgisoTradeWebhookWithDeps records invalid signature failure', async () => {
const updates = []
await assert.rejects(
() => executeAgisoTradeWebhookWithDeps(
createParsedTrade({ signatureValid: false }),
701,
{ requestId: 'req-invalid-signature' },
{
updateWebhookEvent: async (webhookEventId, patch) => {
updates.push({ webhookEventId, patch })
return { id: webhookEventId, ...patch }
},
hasConfiguredOrderItems: async () => {
throw new Error('should not check configured items when signature is invalid')
},
},
),
/验签失败/,
)
assert.deepEqual(updates, [
{
webhookEventId: 701,
patch: {
processed: false,
process_error: '验签失败',
related_order_id: null,
},
},
])
})
test('executeAgisoTradeWebhookWithDeps ignores unconfigured products before upsert', async () => {
const updates = []
let upsertCalled = false
const result = await executeAgisoTradeWebhookWithDeps(
createParsedTrade(),
702,
{ requestId: 'req-unconfigured' },
{
hasConfiguredOrderItems: async (input) => {
assert.equal(input.provider, 'agiso')
assert.equal(input.platform, 'xianyu')
assert.equal(input.shopId, 'shop-9')
assert.equal(input.items.length, 1)
return false
},
updateWebhookEvent: async (webhookEventId, patch) => {
updates.push({ webhookEventId, patch })
return { id: webhookEventId, ...patch }
},
upsertOrderFromWebhook: async () => {
upsertCalled = true
return {}
},
},
)
assert.equal(result.accepted, true)
assert.equal(result.ignored, true)
assert.equal(result.ignoreReason, 'unconfigured_product')
assert.equal(result.orderId, null)
assert.equal(upsertCalled, false)
assert.deepEqual(updates, [
{
webhookEventId: 702,
patch: {
processed: true,
process_error: 'ignored_unconfigured_product',
related_order_id: null,
},
},
])
})
test('executeAgisoTradeWebhookWithDeps enriches incomplete xianyu trade and upserts order', async () => {
const updates = []
const enrichCalls = []
const upsertCalls = []
const parsed = createParsedTrade({
totalAmount: 0,
buyerName: '',
shopName: '',
items: [
{
skuCode: 'dnf-cdk-a',
skuName: 'dnf-cdk-a',
quantity: 1,
spec: {},
},
],
})
const result = await executeAgisoTradeWebhookWithDeps(
parsed,
703,
{ requestId: 'req-success' },
{
hasConfiguredOrderItems: async () => true,
enrichAgisoXianyuTradeOrder: async (input, options) => {
enrichCalls.push({ input, options })
return {
parsed: {
...input,
totalAmount: 1990,
buyerName: '补查买家',
shopName: '补查店铺',
items: [
{
skuCode: 'dnf-cdk-a',
skuName: 'DNF礼包',
quantity: 1,
spec: { title: 'DNF礼包' },
},
],
},
enriched: true,
reason: '',
}
},
upsertOrderFromWebhook: async (input) => {
upsertCalls.push(input)
return {
ignored: false,
ignoreReason: '',
order: { id: 801 },
tasks: [
{ id: 901, task_no: 'DT901', task_status: 'paid' },
],
messageDeliveries: [{ id: 1001 }],
}
},
updateWebhookEvent: async (webhookEventId, patch) => {
updates.push({ webhookEventId, patch })
return { id: webhookEventId, ...patch }
},
},
)
assert.equal(enrichCalls.length, 1)
assert.equal(enrichCalls[0].options.requestId, 'req-success')
assert.equal(upsertCalls.length, 1)
assert.equal(upsertCalls[0].totalAmount, 1990)
assert.equal(upsertCalls[0].buyerName, '补查买家')
assert.equal(result.accepted, true)
assert.equal(result.ignored, false)
assert.equal(result.enriched, true)
assert.equal(result.orderId, 801)
assert.equal(result.totalAmountFen, 1990)
assert.equal(result.taskCount, 1)
assert.deepEqual(result.tasks, [
{
taskId: 901,
taskNo: 'DT901',
status: 'paid',
},
])
assert.deepEqual(updates, [
{
webhookEventId: 703,
patch: {
processed: true,
process_error: '',
related_order_id: 801,
},
},
])
})
@@ -365,6 +365,14 @@
- `npm run typecheck`
- `npm run build`
- `npm test` 共 136 个用例通过
63. webhook service 迁移前补编排测试:
- 新增 `executeAgisoTradeWebhookWithDeps`,便于隔离 webhook event repository、商品匹配、订单 upsert 与订单详情补查依赖
- 覆盖验签失败落错误、未配置商品提前忽略、咸鱼订单补查后 upsert 三个关键分支
64. Docker 内验证通过:
- `src/services/order/webhook-service.test.js` 共 9 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 139 个用例通过
## 下一步建议