Files
order_site/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts
T

380 lines
13 KiB
TypeScript

import {
findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByTaskId,
listKuaishouIndustryVouchersForAdmin,
updateKuaishouIndustryVoucherByCode,
type KuaishouIndustryVoucherAdminListQuery,
} from '../../repositories/kuaishou-industry-voucher-repo.js'
import { getOrderById } from '../../repositories/order-repo.js'
import { getTaskById } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js'
import { maskSecret } from '../../utils/masking.js'
import { checkKuaishouIndustryEticketAvailable } from '../platforms/kuaishou-industry/check-available-service.js'
import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js'
import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js'
import {
approveKuaishouIndustryRefund,
disagreeKuaishouIndustryRefund,
listKuaishouIndustryRefunds,
} from '../platforms/kuaishou-industry/refund-service.js'
import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/reverse-callback-service.js'
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
type JsonObject = Record<string, any>
export async function listAdminKuaishouIndustryVouchers(
input: KuaishouIndustryVoucherAdminListQuery = {},
) {
const result = await listKuaishouIndustryVouchersForAdmin(input)
return {
items: result.items.map(mapAdminKuaishouIndustryVoucher),
pagination: {
page: result.page,
pageSize: result.pageSize,
total: result.total,
},
}
}
export async function listAdminKuaishouIndustryRefunds(input: JsonObject = {}) {
assertRequired(input.beginTime, '开始时间未填写')
assertRequired(input.endTime, '结束时间未填写')
return mapOpenApiResult(await listKuaishouIndustryRefunds(input))
}
export async function approveAdminKuaishouIndustryRefund(input: JsonObject = {}) {
assertRequired(input.refundId, '退款单编号未填写')
assertRequired(input.refundAmount, '退款金额未填写')
return mapOpenApiResult(await approveKuaishouIndustryRefund(input))
}
export async function disagreeAdminKuaishouIndustryRefund(input: JsonObject = {}) {
assertRequired(input.refundId, '退款单编号未填写')
assertRequired(input.sellerDisagreeReason, '拒绝原因未填写')
assertRequired(input.sellerDisagreeDesc, '拒绝说明未填写')
assertRequired(input.status, '退款单当前状态未填写')
assertRequired(input.negotiateStatus, '协商状态未填写')
return mapOpenApiResult(await disagreeKuaishouIndustryRefund(input))
}
export async function checkAdminKuaishouIndustryVoucherAvailable(input: JsonObject = {}) {
const voucher = await resolveOptionalVoucher(input)
const payload = buildVoucherOpenApiPayload(input, voucher)
assertRequired(payload.sellerId, '卖家编号未填写')
assertRequired(payload.eticketType || payload.bizTypeCode, '电子凭证类型未填写')
assertEtickets(payload.etickets, '电子凭证列表未填写')
return mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload))
}
export async function reverseAdminKuaishouIndustryVoucher(input: JsonObject = {}) {
const voucher = await resolveOptionalVoucher(input)
const payload = buildVoucherOpenApiPayload(input, voucher)
assertRequired(payload.oid, '订单号未填写')
assertEtickets(payload.etickets, '冲正券码列表未填写')
const result = await reverseKuaishouIndustryCallback(payload)
let updatedVoucher: KuaishouIndustryVoucherRow | null = null
if (result.success && voucher) {
updatedVoucher = await updateKuaishouIndustryVoucherByCode(voucher.voucher_code, {
status: 'UNUSED',
consumeSerialNum: '',
consumeDetailsJson: [],
consumedAt: null,
destroyedAt: null,
updatedAt: new Date().toISOString(),
})
}
return {
...mapOpenApiResult(result),
voucher: updatedVoucher ? mapAdminKuaishouIndustryVoucher(updatedVoucher) : null,
}
}
export async function consumeAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) {
const voucher = await resolveRequiredVoucher(input)
const task = voucher.task_id ? await getTaskById(voucher.task_id) : null
const consumeInput: Parameters<typeof consumeKuaishouIndustryVoucher>[1] = {
source: 'admin_tool_manual_consume',
token: String(input.token || voucher.token || '').trim(),
consumeType: String(input.consumeType || 'delivery').trim() || 'delivery',
consumeTime: Date.now(),
...(input.serialNum ? { serialNum: String(input.serialNum).trim() } : {}),
...(input.eticketType ? { eticketType: String(input.eticketType).trim() } : {}),
...(input.storeName ? { storeName: String(input.storeName).trim() } : {}),
...(input.storeAddress ? { storeAddress: String(input.storeAddress).trim() } : {}),
...(input.expressCode ? { expressCode: String(input.expressCode).trim() } : {}),
...(input.expressNo ? { expressNo: String(input.expressNo).trim() } : {}),
}
if (task) {
consumeInput.task = task
}
const result = await consumeKuaishouIndustryVoucher(voucher, consumeInput)
if (!result.ok || !result.voucher) {
throw createHttpError(result.errorMessage || '电子凭证核销失败', {
statusCode: 409,
errorCode: 'admin_kuaishou_industry_consume_failed',
})
}
return {
success: true,
voucher: mapAdminKuaishouIndustryVoucher(result.voucher),
task: task ? mapTaskReference(task) : null,
}
}
export async function resendAdminKuaishouIndustryVoucherCode(input: JsonObject = {}) {
const voucher = await resolveRequiredVoucher(input)
const order = voucher.order_id ? await getOrderById(voucher.order_id) : null
const result = await resendKuaishouIndustryVoucherSendCallback({
voucherCode: voucher.voucher_code,
oid: voucher.oid,
preferredTotalGoodsValue: Number(order?.total_amount || 0) || 0,
})
return {
success: result.success,
response: 'response' in result ? result.response || null : null,
error: result.error || '',
voucher: result.voucher ? mapAdminKuaishouIndustryVoucher(result.voucher) : null,
}
}
function buildVoucherOpenApiPayload(
input: JsonObject,
voucher: KuaishouIndustryVoucherRow | null,
): JsonObject {
const etickets = normalizeAdminEtickets(input.etickets)
const voucherCode = String(voucher?.voucher_code || input.voucherCode || '').trim()
return {
...input,
sellerId: String(input.sellerId || voucher?.seller_id || '').trim(),
oid: String(input.oid || input.orderId || voucher?.oid || '').trim(),
orderId: String(input.orderId || input.oid || voucher?.oid || '').trim(),
token: String(input.token || voucher?.token || '').trim(),
serialNum: String(input.serialNum || voucher?.consume_serial_num || '').trim(),
etickets: etickets.length > 0
? etickets
: voucherCode
? [{ id: voucherCode, code: voucherCode, num: 1 }]
: [],
}
}
async function resolveOptionalVoucher(input: JsonObject): Promise<KuaishouIndustryVoucherRow | null> {
const voucherCode = String(input.voucherCode || '').trim()
if (!voucherCode) {
return null
}
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, String(input.oid || input.orderId || '').trim())
if (!voucher) {
throw createHttpError('电子凭证不存在', {
statusCode: 404,
errorCode: 'admin_kuaishou_industry_voucher_not_found',
})
}
return voucher
}
async function resolveRequiredVoucher(input: JsonObject): Promise<KuaishouIndustryVoucherRow> {
const voucher = await resolveOptionalVoucher(input)
if (voucher) {
return voucher
}
const taskId = Number(input.taskId || 0)
if (Number.isFinite(taskId) && taskId > 0) {
const vouchers = await listKuaishouIndustryVouchersByTaskId(Math.trunc(taskId))
return resolveSingleVoucher(vouchers, '当前任务没有关联电子凭证', '当前任务关联多个电子凭证,请指定券码')
}
const oid = String(input.oid || input.orderId || '').trim()
if (oid) {
const vouchers = await listKuaishouIndustryVouchersByOid(oid)
return resolveSingleVoucher(vouchers, '当前订单没有关联电子凭证', '当前订单关联多个电子凭证,请指定券码')
}
throw createHttpError('请填写券码、任务 ID 或订单号', {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_voucher_selector_missing',
})
}
function resolveSingleVoucher(
vouchers: KuaishouIndustryVoucherRow[],
missingMessage: string,
multipleMessage: string,
): KuaishouIndustryVoucherRow {
if (vouchers.length === 0) {
throw createHttpError(missingMessage, {
statusCode: 404,
errorCode: 'admin_kuaishou_industry_voucher_not_found',
})
}
if (vouchers.length > 1) {
throw createHttpError(multipleMessage, {
statusCode: 409,
errorCode: 'admin_kuaishou_industry_voucher_ambiguous',
})
}
return vouchers[0] as KuaishouIndustryVoucherRow
}
function mapOpenApiResult(result: KuaishouIndustryOpenApiCallResult) {
return {
success: result.success,
response: result.response || null,
error: result.error || '',
request: sanitizeOpenApiRequest(result.request),
durationMs: result.durationMs || 0,
httpStatus: result.httpStatus || 0,
skippedReason: result.skippedReason || '',
}
}
function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | null {
if (!request) {
return null
}
return {
...request,
access_token: maskSecret(request.access_token),
sign: maskSecret(request.sign),
}
}
function mapAdminKuaishouIndustryVoucher(voucher: KuaishouIndustryVoucherRow) {
return {
id: voucher.id,
voucherCode: voucher.voucher_code,
oid: voucher.oid,
orderId: voucher.order_id,
taskId: voucher.task_id,
unitIndex: voucher.unit_index,
sellerId: voucher.seller_id,
tokenMasked: maskSecret(voucher.token),
status: voucher.status,
validStartTime: Number(voucher.valid_start_time || 0) || 0,
validEndTime: Number(voucher.valid_end_time || 0) || 0,
consumeSerialNum: voucher.consume_serial_num,
consumeDetails: parseJsonArray(voucher.consume_details_json),
consumedAt: voucher.consumed_at,
destroyedAt: voucher.destroyed_at,
sendCallbackStatus: voucher.send_callback_status,
sendCallbackAttemptCount: voucher.send_callback_attempt_count,
sendCallbackLastError: voucher.send_callback_last_error,
sendCallbackSentAt: voucher.send_callback_sent_at,
rawPayload: parseJsonObject(voucher.raw_payload_json),
createdAt: voucher.created_at,
updatedAt: voucher.updated_at,
}
}
function mapTaskReference(task: TaskRow) {
return {
taskId: task.id,
taskNo: task.task_no,
status: task.task_status,
deliveryStatus: task.delivery_status,
}
}
function normalizeAdminEtickets(value: unknown): JsonObject[] {
if (!Array.isArray(value)) {
return []
}
const items: JsonObject[] = []
for (const item of value) {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
continue
}
const current = item as JsonObject
const id = String(current.id || '').trim()
const code = String(current.code || '').trim()
const num = Number(current.num || 1)
if (!id && !code) {
continue
}
items.push({
...(id ? { id } : {}),
...(code ? { code } : {}),
num: Number.isInteger(num) && num > 0 ? num : 1,
})
}
return items
}
function assertRequired(value: unknown, message: string) {
if (value === undefined || value === null || String(value).trim() === '') {
throw createHttpError(message, {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_missing_required_field',
})
}
}
function assertEtickets(value: unknown, message: string) {
if (!Array.isArray(value) || value.length === 0) {
throw createHttpError(message, {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_missing_etickets',
})
}
}
function parseJsonObject(value: unknown): JsonObject {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as JsonObject
}
try {
const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
} catch {
return {}
}
}
function parseJsonArray(value: unknown): JsonObject[] {
if (Array.isArray(value)) {
return value.filter((item): item is JsonObject =>
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
)
}
try {
const parsed = JSON.parse(String(value || '[]'))
return Array.isArray(parsed)
? parsed.filter((item): item is JsonObject =>
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
)
: []
} catch {
return []
}
}