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