简化履约匹配并支持mock订单测试
This commit is contained in:
@@ -2,8 +2,10 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
consumeKuaishouEticket,
|
||||
mapKuaishouEticketDetailResult,
|
||||
normalizeKuaishouEticketApiResult,
|
||||
queryKuaishouEticketConsumeDetail,
|
||||
resolveKuaishouEticketConsumePayload,
|
||||
} from './consume-service.js'
|
||||
|
||||
@@ -70,3 +72,50 @@ test('resolveKuaishouEticketConsumePayload falls back to detail result', () => {
|
||||
formToken: '1777716306154',
|
||||
})
|
||||
})
|
||||
|
||||
test('queryKuaishouEticketConsumeDetail accepts mock ticket code outside production', async () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
try {
|
||||
const result = await queryKuaishouEticketConsumeDetail({
|
||||
eTicketId: 'MOCK-CLAIM-001',
|
||||
goodsTitle: '套装-浪漫天命',
|
||||
})
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(result.eTicketId, 'MOCK-CLAIM-001')
|
||||
assert.equal(result.detail?.leftCount, 1)
|
||||
assert.equal(result.goods?.itemTitle, '套装-浪漫天命')
|
||||
} finally {
|
||||
if (originalNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV
|
||||
} else {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('consumeKuaishouEticket consumes mock ticket code outside production', async () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
try {
|
||||
const result = await consumeKuaishouEticket({
|
||||
eTicketId: 'MOCK-CLAIM-002',
|
||||
oid: 'MOCK-OID-002',
|
||||
formToken: 'MOCK-FORM-002',
|
||||
})
|
||||
|
||||
assert.equal(result.consumed, true)
|
||||
assert.equal(result.eTicketId, 'MOCK-CLAIM-002')
|
||||
assert.equal(result.oid, 'MOCK-OID-002')
|
||||
assert.equal(result.formToken, 'MOCK-FORM-002')
|
||||
} finally {
|
||||
if (originalNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV
|
||||
} else {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { kuaishouEticketRequest } from './http-client.js'
|
||||
import { resolveKuaishouEticketConfig } from './helpers.js'
|
||||
import {
|
||||
buildMockKuaishouEticketConsumeResult,
|
||||
buildMockKuaishouEticketDetailResult,
|
||||
isKuaishouEticketMockTicketCode,
|
||||
} from './mock-ticket-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
@@ -18,6 +23,14 @@ type KuaishouEticketDetailResult = {
|
||||
}
|
||||
|
||||
export async function queryKuaishouEticketConsumeDetail(payload: JsonObject = {}) {
|
||||
if (isKuaishouEticketMockTicketCode(payload.eTicketId)) {
|
||||
return buildMockKuaishouEticketDetailResult({
|
||||
baseUrl: String(payload.baseUrl || '').trim(),
|
||||
eTicketId: String(payload.eTicketId || '').trim(),
|
||||
goodsTitle: String(payload.goodsTitle || '').trim(),
|
||||
})
|
||||
}
|
||||
|
||||
const context = resolveKuaishouEticketActionContext(payload)
|
||||
const response = await kuaishouEticketRequest(context.detailPath, {
|
||||
baseUrl: context.baseUrl,
|
||||
@@ -35,6 +48,17 @@ export async function queryKuaishouEticketConsumeDetail(payload: JsonObject = {}
|
||||
}
|
||||
|
||||
export async function consumeKuaishouEticket(payload: JsonObject = {}) {
|
||||
if (isKuaishouEticketMockTicketCode(payload.eTicketId)) {
|
||||
return buildMockKuaishouEticketConsumeResult({
|
||||
baseUrl: String(payload.baseUrl || '').trim(),
|
||||
eTicketId: String(payload.eTicketId || '').trim(),
|
||||
oid: String(payload.oid || '').trim(),
|
||||
formToken: String(payload.formToken || '').trim(),
|
||||
num: payload.num,
|
||||
storeId: String(payload.storeId || '').trim(),
|
||||
})
|
||||
}
|
||||
|
||||
const context = resolveKuaishouEticketActionContext(payload)
|
||||
const detailResult = shouldResolveConsumeDetail(payload)
|
||||
? await queryKuaishouEticketConsumeDetail({
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import process from 'node:process'
|
||||
|
||||
import { isProductionLike } from '../../../config/runtime-validation.js'
|
||||
|
||||
type RuntimeEnvironment = {
|
||||
NODE_ENV?: string
|
||||
}
|
||||
|
||||
type MockTicketDetailOptions = {
|
||||
eTicketId: string
|
||||
baseUrl?: string
|
||||
oid?: string
|
||||
formToken?: string
|
||||
leftCount?: number
|
||||
goodsTitle?: string
|
||||
}
|
||||
|
||||
type MockTicketConsumeOptions = {
|
||||
eTicketId: string
|
||||
baseUrl?: string
|
||||
oid?: string
|
||||
formToken?: string
|
||||
num?: number
|
||||
storeId?: string
|
||||
}
|
||||
|
||||
export function isKuaishouEticketMockTicketCode(
|
||||
ticketCode: unknown,
|
||||
env: RuntimeEnvironment = process.env,
|
||||
): boolean {
|
||||
const normalized = String(ticketCode || '').trim().toUpperCase()
|
||||
|
||||
if (!normalized || isProductionLike(env)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return normalized === 'MOCK' || normalized.startsWith('MOCK-') || normalized.startsWith('MOCK_')
|
||||
}
|
||||
|
||||
export function buildMockKuaishouEticketDetailResult(options: MockTicketDetailOptions) {
|
||||
const eTicketId = String(options.eTicketId || '').trim()
|
||||
const oid = String(options.oid || `MOCK-OID-${eTicketId}`).trim()
|
||||
const formToken = String(options.formToken || `MOCK-FORM-${eTicketId}`).trim()
|
||||
const leftCount = normalizePositiveInteger(options.leftCount, 1)
|
||||
const goodsTitle = String(options.goodsTitle || 'MOCK 快手商品').trim()
|
||||
|
||||
return {
|
||||
baseUrl: String(options.baseUrl || 'mock://kuaishou-eticket').trim(),
|
||||
eTicketId,
|
||||
ok: true,
|
||||
alreadyConsumed: false,
|
||||
result: 1,
|
||||
errorMessage: '',
|
||||
requestId: `mock-${Date.now()}`,
|
||||
serverTimestamp: new Date().toISOString(),
|
||||
detail: {
|
||||
uid: 'mock',
|
||||
fulfillDetailId: `MOCK-FULFILL-${eTicketId}`,
|
||||
sellerId: 'mock',
|
||||
formToken,
|
||||
validEndTime: '',
|
||||
validStartTime: '',
|
||||
leftReverseCount: 0,
|
||||
eTicketId,
|
||||
oid,
|
||||
totalCount: leftCount,
|
||||
leftCount,
|
||||
status: 'MOCK',
|
||||
},
|
||||
goods: {
|
||||
itemId: 'mock',
|
||||
itemPicUrl: '',
|
||||
itemTitle: goodsTitle,
|
||||
price: '',
|
||||
skuDesc: goodsTitle,
|
||||
skuId: 'mock',
|
||||
},
|
||||
raw: {
|
||||
mock: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMockKuaishouEticketConsumeResult(options: MockTicketConsumeOptions) {
|
||||
const eTicketId = String(options.eTicketId || '').trim()
|
||||
const oid = String(options.oid || `MOCK-OID-${eTicketId}`).trim()
|
||||
const formToken = String(options.formToken || `MOCK-FORM-${eTicketId}`).trim()
|
||||
const num = normalizePositiveInteger(options.num, 1)
|
||||
|
||||
return {
|
||||
baseUrl: String(options.baseUrl || 'mock://kuaishou-eticket').trim(),
|
||||
eTicketId,
|
||||
oid,
|
||||
formToken,
|
||||
num,
|
||||
storeId: String(options.storeId || '0').trim() || '0',
|
||||
ok: true,
|
||||
consumed: true,
|
||||
alreadyConsumed: false,
|
||||
result: 1,
|
||||
errorMessage: '',
|
||||
requestId: `mock-${Date.now()}`,
|
||||
serverTimestamp: new Date().toISOString(),
|
||||
detail: {
|
||||
eTicketId,
|
||||
oid,
|
||||
formToken,
|
||||
leftCount: num,
|
||||
},
|
||||
raw: {
|
||||
mock: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value: unknown, fallback: number) {
|
||||
const normalized = Number(value)
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return Math.floor(normalized)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { normalizeOpen91CreatePayload } from '../../open-91/payload.js'
|
||||
import { buildOpen91OutTradeNo } from '../../open-91/response.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { parseAmountToFen } from '../../../utils/money.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js'
|
||||
|
||||
@@ -21,6 +22,7 @@ type JsonObject = Record<string, any>
|
||||
export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) {
|
||||
const normalized = normalizeOpen91CreatePayload(payload)
|
||||
const productNo = normalized.productNo
|
||||
const maxAmountFen = parseAmountToFen(normalized.maxAmount)
|
||||
|
||||
return {
|
||||
provider: OPEN_91_PROVIDER,
|
||||
@@ -33,7 +35,7 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
|
||||
buyerId: '',
|
||||
buyerName: '',
|
||||
receiverContact: '',
|
||||
totalAmount: 0,
|
||||
totalAmount: maxAmountFen,
|
||||
currency: 'CNY',
|
||||
paidAt: nowIso(),
|
||||
rawPayload: {
|
||||
@@ -54,11 +56,14 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
|
||||
source: OPEN_91_PROVIDER,
|
||||
orderNo: normalized.orderNo,
|
||||
productNo,
|
||||
maxAmount: normalized.maxAmount,
|
||||
},
|
||||
snapshot: {
|
||||
source: OPEN_91_PROVIDER,
|
||||
orderNo: normalized.orderNo,
|
||||
productNo,
|
||||
maxAmount: normalized.maxAmount,
|
||||
maxAmountFen,
|
||||
externalItemId: productNo,
|
||||
externalSkuCode: productNo,
|
||||
externalSkuName: productNo,
|
||||
@@ -95,7 +100,7 @@ export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config:
|
||||
buyerId: '',
|
||||
buyerName: '',
|
||||
receiverContact: '',
|
||||
totalAmount: 0,
|
||||
totalAmount: event.totalAmount,
|
||||
currency: 'CNY',
|
||||
rawPayloadJson,
|
||||
paidAt: existing?.paid_at || now,
|
||||
|
||||
Reference in New Issue
Block a user