diff --git a/apps/backend/src/services/claim/claim-session-service.test.js b/apps/backend/src/services/claim/claim-session-service.test.js index ef49410e..79307cc2 100644 --- a/apps/backend/src/services/claim/claim-session-service.test.js +++ b/apps/backend/src/services/claim/claim-session-service.test.js @@ -169,3 +169,105 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and ], ) }) + +test('redeemClaimTaskWithInventoryFallbackWithDeps waits for inventory when used code has no replacement', async () => { + const events = [] + const consumed = [] + const reserveCalls = [] + + const context = { + task: { + id: 57, + browser_session_id: 'txbs-used', + }, + orderItem: { + sku_code: 'df-cdk-used', + }, + } + const initialInventoryItem = { + id: 7, + display_value: 'USED1234CODE5678', + credential_type: 'tencent_code', + inventory_group_code: 'B组', + } + + let thrown = null + try { + await redeemClaimTaskWithInventoryFallbackWithDeps( + context, + initialInventoryItem, + { + redeemTencentBrowserSession: async () => ({ + redeem: { + final: { + redeem: { + popup: { + text: '兑换结果', + detail: '兑换码已使用。', + }, + }, + }, + }, + }), + markInventoryItemConsumed: async (inventoryItemId, reason, updatedAt) => { + consumed.push({ inventoryItemId, reason, updatedAt }) + return { id: inventoryItemId, status: 'consumed', invalid_reason: reason, updated_at: updatedAt } + }, + reserveInventoryForTask: async (payload) => { + reserveCalls.push(payload) + return null + }, + createTaskEvent: async (taskId, eventType, payload, createdAt) => { + events.push({ taskId, eventType, payload, createdAt }) + }, + nowIso: () => '2026-04-14T12:05:00.000Z', + }, + ) + } catch (error) { + thrown = error + } + + assert.ok(thrown) + assert.equal(thrown.errorCode, 'claim_inventory_replacement_exhausted') + assert.equal(thrown.redeemTaskState.taskStatus, 'waiting_inventory') + assert.equal(thrown.redeemTaskState.inventoryStatus, 'pending') + assert.equal(thrown.redeemTaskState.deliveryStatus, 'pending') + assert.equal(thrown.redeemTaskState.attempts.length, 1) + assert.deepEqual(consumed, [ + { + inventoryItemId: 7, + reason: '兑换码已使用。', + updatedAt: '2026-04-14T12:05:00.000Z', + }, + ]) + assert.deepEqual(reserveCalls, [ + { + skuCode: 'df-cdk-used', + taskId: 57, + credentialType: 'tencent_code', + roleKey: 'primary_code', + inventoryGroupCodes: ['B组'], + }, + ]) + assert.deepEqual( + events.map((event) => ({ + taskId: event.taskId, + eventType: event.eventType, + payload: event.payload, + createdAt: event.createdAt, + })), + [ + { + taskId: 57, + eventType: 'claim_redeem_code_used', + payload: { + inventoryItemId: 7, + codeMasked: 'USED****5678', + resultCode: '', + resultMessage: '兑换码已使用。', + }, + createdAt: '2026-04-14T12:05:00.000Z', + }, + ], + ) +}) diff --git a/apps/backend/src/services/order/webhook-service.test.js b/apps/backend/src/services/order/webhook-service.test.js index ffd4a3d5..7b557857 100644 --- a/apps/backend/src/services/order/webhook-service.test.js +++ b/apps/backend/src/services/order/webhook-service.test.js @@ -4,6 +4,7 @@ import crypto from 'node:crypto' import { runtimeConfig } from '../../config/runtime.js' import { + buildWebhookEventInput, parseAgisoTradeRequest, resolveAgisoTradeIgnoreReason, shouldEnrichAgisoXianyuTrade, @@ -147,3 +148,35 @@ test('verifyAgisoSignatureWithSecret supports documented and legacy signatures', false, ) }) + +test('buildWebhookEventInput preserves raw request payload for later replay', () => { + const input = buildWebhookEventInput( + { + headers: { 'x-request-id': 'req-1' }, + query: { timestamp: '1713097840', sign: 'bad-sign' }, + body: { json: '{"biz_order_id":"4502259793206016214"}' }, + }, + { + provider: 'agiso', + platform: 'xianyu', + shopId: 'shop-9', + shopName: '咸鱼店铺', + eventType: 'payment_success', + eventKey: 'agiso:xianyu:4502259793206016214:payment_success', + signatureValid: false, + }, + ) + + assert.equal(input.provider, 'agiso') + assert.equal(input.platform, 'xianyu') + assert.equal(input.eventType, 'payment_success') + assert.equal(input.eventKey, 'agiso:xianyu:4502259793206016214:payment_success') + assert.equal(input.signatureValid, false) + assert.equal(input.processed, false) + assert.equal(input.processError, '') + assert.equal(input.relatedOrderId, null) + assert.deepEqual(JSON.parse(input.headersJson), { 'x-request-id': 'req-1' }) + assert.deepEqual(JSON.parse(input.queryJson), { timestamp: '1713097840', sign: 'bad-sign' }) + assert.deepEqual(JSON.parse(input.bodyJson), { json: '{"biz_order_id":"4502259793206016214"}' }) + assert.match(input.createdAt, /^\d{4}-\d{2}-\d{2}T/) +}) diff --git a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js index 5a99fcfd..2714517f 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js @@ -32,6 +32,18 @@ test('hasAgisoAutoDeliverySucceeded detects successful context on any task', () ]), false) }) +test('hasAgisoAutoDeliverySucceeded handles string and invalid task context payloads', () => { + assert.equal(hasAgisoAutoDeliverySucceeded([ + { context_json: '{bad-json' }, + { context_json: JSON.stringify({ agisoAutoDelivery: { status: 'success' } }) }, + ]), true) + + assert.equal(hasAgisoAutoDeliverySucceeded([ + { context_json: '{bad-json' }, + { context_json: JSON.stringify({ agisoAutoDelivery: { status: 'skipped' } }) }, + ]), false) +}) + test('isAgisoAutoDeliverySuccess accepts empty successful payloads and explicit success codes', () => { assert.equal(isAgisoAutoDeliverySuccess(200, {}), true) assert.equal(isAgisoAutoDeliverySuccess(200, { IsSuccess: true }), true) diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 081802e4..c3acf22e 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -247,13 +247,22 @@ - `npm run typecheck` - `npm run build` - `npm test` +23. 高风险链路补测试: + - webhook 事件入库 payload 可稳定保留原始 headers/query/body,便于后续 replay + - 库存换码在“兑换码已使用且无替换库存”时进入等待库存状态,并记录已使用事件 + - Agiso 自动发货成功标记可从字符串 context 中识别,且会忽略无效 JSON +24. Docker 内验证通过: + - 相关 3 个测试文件共 14 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 125 个用例通过 ## 下一步建议 第一批继续推进时,建议按这个顺序: -1. 为 webhook、库存换码、自动发货补测试 -2. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块 +1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块 +2. 逐步移除服务层 `@ts-nocheck`,优先处理自动发货、claim session、webhook service ## 执行原则