后端补充Webhook编排测试
This commit is contained in:
@@ -80,6 +80,19 @@ export function buildWebhookEventInput(requestLike, parsed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' } = {}) {
|
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 {
|
try {
|
||||||
if (!parsed.signatureValid) {
|
if (!parsed.signatureValid) {
|
||||||
throw createHttpError('验签失败', {
|
throw createHttpError('验签失败', {
|
||||||
@@ -97,7 +110,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
|||||||
|
|
||||||
const ignoreReason = resolveAgisoTradeIgnoreReason(parsed)
|
const ignoreReason = resolveAgisoTradeIgnoreReason(parsed)
|
||||||
if (ignoreReason) {
|
if (ignoreReason) {
|
||||||
await updateWebhookEvent(webhookEventId, {
|
await updateEvent(webhookEventId, {
|
||||||
processed: true,
|
processed: true,
|
||||||
process_error: `ignored_${ignoreReason}`,
|
process_error: `ignored_${ignoreReason}`,
|
||||||
related_order_id: null,
|
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,
|
provider: parsed.provider,
|
||||||
platform: parsed.platform,
|
platform: parsed.platform,
|
||||||
shopId: parsed.shopId,
|
shopId: parsed.shopId,
|
||||||
@@ -146,7 +159,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!shouldProcess) {
|
if (!shouldProcess) {
|
||||||
await updateWebhookEvent(webhookEventId, {
|
await updateEvent(webhookEventId, {
|
||||||
processed: true,
|
processed: true,
|
||||||
process_error: 'ignored_unconfigured_product',
|
process_error: 'ignored_unconfigured_product',
|
||||||
related_order_id: null,
|
related_order_id: null,
|
||||||
@@ -187,9 +200,9 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
|||||||
const enriched = await enrichTradeBeforeUpsert(parsed, {
|
const enriched = await enrichTradeBeforeUpsert(parsed, {
|
||||||
requestId,
|
requestId,
|
||||||
webhookEventId,
|
webhookEventId,
|
||||||
})
|
}, deps)
|
||||||
const result = await upsertOrderFromWebhook(enriched.parsed)
|
const result = await upsertOrder(enriched.parsed)
|
||||||
await updateWebhookEvent(webhookEventId, {
|
await updateEvent(webhookEventId, {
|
||||||
processed: true,
|
processed: true,
|
||||||
process_error: result.ignored ? 'ignored_unconfigured_product' : '',
|
process_error: result.ignored ? 'ignored_unconfigured_product' : '',
|
||||||
related_order_id: result.order?.id || null,
|
related_order_id: result.order?.id || null,
|
||||||
@@ -239,7 +252,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateWebhookEvent(webhookEventId, {
|
await updateEvent(webhookEventId, {
|
||||||
processed: false,
|
processed: false,
|
||||||
process_error: error instanceof Error ? error.message : String(error || ''),
|
process_error: error instanceof Error ? error.message : String(error || ''),
|
||||||
related_order_id: null,
|
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') {
|
if (parsed.provider !== 'agiso' || parsed.platform !== 'xianyu') {
|
||||||
return { parsed, enriched: false, reason: 'not_supported' }
|
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' }
|
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
|
const nextParsed = detailResult?.parsed || parsed
|
||||||
|
|
||||||
logWebhook('[webhook-service/agiso]', 'Agiso webhook 订单补查结束', {
|
logWebhook('[webhook-service/agiso]', 'Agiso webhook 订单补查结束', {
|
||||||
|
|||||||
@@ -5,12 +5,46 @@ import crypto from 'node:crypto'
|
|||||||
import { runtimeConfig } from '../../config/runtime.js'
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
import {
|
import {
|
||||||
buildWebhookEventInput,
|
buildWebhookEventInput,
|
||||||
|
executeAgisoTradeWebhookWithDeps,
|
||||||
parseAgisoTradeRequest,
|
parseAgisoTradeRequest,
|
||||||
resolveAgisoTradeIgnoreReason,
|
resolveAgisoTradeIgnoreReason,
|
||||||
shouldEnrichAgisoXianyuTrade,
|
shouldEnrichAgisoXianyuTrade,
|
||||||
verifyAgisoSignatureWithSecret,
|
verifyAgisoSignatureWithSecret,
|
||||||
} from './webhook-service.js'
|
} 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', () => {
|
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 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'
|
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.deepEqual(JSON.parse(input.bodyJson), { json: '{"biz_order_id":"4502259793206016214"}' })
|
||||||
assert.match(input.createdAt, /^\d{4}-\d{2}-\d{2}T/)
|
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 typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 136 个用例通过
|
- `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