286 lines
8.4 KiB
TypeScript
286 lines
8.4 KiB
TypeScript
import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
|
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
|
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
|
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
|
|
import { logIntegration } from '../../utils/logger.js'
|
|
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
|
import {
|
|
OPEN_91_MANUAL_FAILED_STATUS,
|
|
OPEN_91_PENDING_CONFIG_STATUS,
|
|
} from '../platforms/ninetyone/order-service.js'
|
|
import {
|
|
isKuaishouIndustryVoucherSendCallbackSuccess,
|
|
resolveKuaishouIndustryVoucherSendCallbackMessage,
|
|
} from '../platforms/kuaishou-industry/voucher-service.js'
|
|
import {
|
|
assertOpen91Config,
|
|
OPEN_91_DEFAULT_FAIL_CODE,
|
|
OPEN_91_PLATFORM,
|
|
OPEN_91_PROVIDER,
|
|
} from './config.js'
|
|
import { consumeIndustryVouchersBeforeOpen91Deliver } from './consume-on-deliver.js'
|
|
import {
|
|
assertOpen91QueryPayload,
|
|
assertOpen91Timestamp,
|
|
normalizeOpen91QueryPayload,
|
|
} from './payload.js'
|
|
import { assertOpen91Signature, buildOpen91Cards } from './crypto.js'
|
|
import {
|
|
buildOpen91CardItem,
|
|
buildOpen91OrderCost,
|
|
buildOpen91OutTradeNo,
|
|
buildOpen91SuccessResponse,
|
|
resolveOpen91QueryState,
|
|
} from './response.js'
|
|
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
|
|
|
export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '' } = {}) {
|
|
const config = assertOpen91Config()
|
|
const normalized = normalizeOpen91QueryPayload(payload)
|
|
|
|
assertOpen91QueryPayload(normalized, config)
|
|
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
|
|
assertOpen91Signature(payload, config)
|
|
|
|
const order = await findLatestOrderByPlatformOrderId({
|
|
provider: OPEN_91_PROVIDER,
|
|
platform: OPEN_91_PLATFORM,
|
|
platformOrderId: normalized.orderNo,
|
|
})
|
|
|
|
if (!order) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: '',
|
|
orderStatus: 30,
|
|
failCode: OPEN_91_DEFAULT_FAIL_CODE,
|
|
failReason: '订单不存在',
|
|
})
|
|
}
|
|
|
|
const tasks = await listTasksByOrderId(order.id)
|
|
const industryVoucherState = await resolveOpen91IndustryVoucherState(order, tasks)
|
|
|
|
if (tasks.length === 0) {
|
|
if (String(order.order_status || '').trim() === OPEN_91_PENDING_CONFIG_STATUS) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 10,
|
|
failCode: 0,
|
|
failReason: '',
|
|
orderCost: buildOpen91OrderCost(0),
|
|
cards: '',
|
|
})
|
|
}
|
|
|
|
if (String(order.order_status || '').trim() === OPEN_91_MANUAL_FAILED_STATUS) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 30,
|
|
failCode: OPEN_91_DEFAULT_FAIL_CODE,
|
|
failReason: resolveOpen91OrderFailReason(order) || '商家手动标记无法履约',
|
|
})
|
|
}
|
|
|
|
if (industryVoucherState.voucherCount > 0) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 10,
|
|
failCode: 0,
|
|
failReason: '',
|
|
orderCost: buildOpen91OrderCost(0),
|
|
cards: '',
|
|
})
|
|
}
|
|
}
|
|
|
|
const cardItems = []
|
|
const readyTaskIds = []
|
|
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
|
|
|
|
for (const task of tasks) {
|
|
const deliveryLink = await resolveTaskDeliveryLink(task)
|
|
const claimUrl = String(deliveryLink?.claimUrl || '').trim()
|
|
|
|
if (!claimUrl) {
|
|
continue
|
|
}
|
|
|
|
if (!industryReadyTaskIds.has(Number(task.id))) {
|
|
continue
|
|
}
|
|
|
|
readyTaskIds.push(Number(task.id))
|
|
cardItems.push(
|
|
buildOpen91CardItem({
|
|
claimUrl,
|
|
expireTime: deliveryLink?.expireTime || '',
|
|
}),
|
|
)
|
|
}
|
|
|
|
const queryState = resolveOpen91QueryState({
|
|
tasks,
|
|
readyTaskIds,
|
|
})
|
|
|
|
logIntegration('[open-91/query]', '91卡券订单查询已处理', {
|
|
requestId,
|
|
orderNo: normalized.orderNo,
|
|
orderId: order.id,
|
|
taskCount: tasks.length,
|
|
readyTaskCount: readyTaskIds.length,
|
|
voucherCount: industryVoucherState.voucherCount,
|
|
voucherReadyTaskCount: industryVoucherState.readyTaskIds.length,
|
|
voucherPendingReason: industryVoucherState.pendingReason,
|
|
orderStatus: queryState.orderStatus,
|
|
})
|
|
|
|
if (queryState.orderStatus === 30) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 30,
|
|
failCode: queryState.failCode,
|
|
failReason: queryState.failReason,
|
|
})
|
|
}
|
|
|
|
if (queryState.orderStatus === 10) {
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 10,
|
|
failCode: 0,
|
|
failReason: '',
|
|
orderCost: buildOpen91OrderCost(0),
|
|
cards: '',
|
|
})
|
|
}
|
|
|
|
// 即将交付 cards:先同步核销行业电子凭证,成功后再返回 20,避免未核销发链导致误退款。
|
|
const deliverConsume = await consumeIndustryVouchersBeforeOpen91Deliver({
|
|
order,
|
|
tasks,
|
|
readyTaskIds,
|
|
requestId,
|
|
})
|
|
|
|
if (!deliverConsume.ok) {
|
|
logIntegration('[open-91/query]', '91卡券订单查询因交付前核销失败返回处理中', {
|
|
requestId,
|
|
orderNo: normalized.orderNo,
|
|
orderId: order.id,
|
|
readyTaskCount: readyTaskIds.length,
|
|
failedTaskIds: deliverConsume.failedTaskIds,
|
|
errorMessage: deliverConsume.errorMessage,
|
|
})
|
|
|
|
return buildOpen91SuccessResponse({
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo: buildOpen91OutTradeNo(order),
|
|
orderStatus: 10,
|
|
failCode: 0,
|
|
failReason: '',
|
|
orderCost: buildOpen91OrderCost(0),
|
|
cards: '',
|
|
})
|
|
}
|
|
|
|
const outTradeNo = buildOpen91OutTradeNo(order)
|
|
const encryptedCards = buildOpen91Cards(cardItems, config)
|
|
const responseData = {
|
|
orderNo: normalized.orderNo,
|
|
outTradeNo,
|
|
orderStatus: 20,
|
|
failCode: 0,
|
|
failReason: '',
|
|
orderCost: buildOpen91OrderCost(0),
|
|
cards: encryptedCards,
|
|
}
|
|
|
|
logIntegration('[open-91/query]', '91卡券订单查询返回 cards 已生成', {
|
|
requestId,
|
|
orderNo: normalized.orderNo,
|
|
orderId: order.id,
|
|
outTradeNo,
|
|
cardsEncoding: config.cardsEncoding,
|
|
cardsPlain: cardItems,
|
|
cardsEncrypted: encryptedCards,
|
|
deliverConsumedTaskIds: deliverConsume.consumedTaskIds,
|
|
deliverAlreadyConsumedTaskIds: deliverConsume.alreadyConsumedTaskIds,
|
|
responseData,
|
|
})
|
|
|
|
return buildOpen91SuccessResponse(responseData)
|
|
}
|
|
|
|
function resolveOpen91OrderFailReason(order: OrderRow) {
|
|
const payload = parseJsonObject(order?.raw_payload_json)
|
|
return String(payload.manualFailedReason || '').trim()
|
|
}
|
|
|
|
async function resolveOpen91IndustryVoucherState(order: OrderRow, tasks: TaskRow[]) {
|
|
const vouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id)
|
|
const voucherByTaskId = new Map<number, KuaishouIndustryVoucherRow>()
|
|
const voucherByUnitIndex = new Map<number, KuaishouIndustryVoucherRow>()
|
|
|
|
for (const voucher of vouchers) {
|
|
const taskId = Number(voucher.task_id || 0)
|
|
const unitIndex = Number(voucher.unit_index || 0)
|
|
if (taskId > 0 && !voucherByTaskId.has(taskId)) {
|
|
voucherByTaskId.set(taskId, voucher)
|
|
}
|
|
if (unitIndex > 0 && !voucherByUnitIndex.has(unitIndex)) {
|
|
voucherByUnitIndex.set(unitIndex, voucher)
|
|
}
|
|
}
|
|
|
|
const readyTaskIds: number[] = []
|
|
let pendingReason = ''
|
|
|
|
for (const task of tasks) {
|
|
const taskId = Number(task.id || 0)
|
|
const unitIndex = Number(task.unit_index || 0)
|
|
const voucher = voucherByTaskId.get(taskId) || voucherByUnitIndex.get(unitIndex) || null
|
|
|
|
if (!voucher) {
|
|
pendingReason = pendingReason || '电子凭证发码通知尚未完成'
|
|
continue
|
|
}
|
|
|
|
if (!isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) {
|
|
pendingReason = pendingReason || resolveKuaishouIndustryVoucherSendCallbackMessage(voucher)
|
|
continue
|
|
}
|
|
|
|
readyTaskIds.push(taskId)
|
|
}
|
|
|
|
return {
|
|
voucherCount: vouchers.length,
|
|
readyTaskIds,
|
|
pendingReason,
|
|
}
|
|
}
|
|
|
|
function parseJsonObject(value: unknown): JsonObject {
|
|
if (!value) {
|
|
return {}
|
|
}
|
|
|
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
return value
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(String(value || '{}'))
|
|
return asJsonObject(parsed)
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|