import { asJsonObject, type JsonObject } from '../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByTaskId, listKuaishouIndustryVouchersForAdmin, updateKuaishouIndustryVoucherByCode, type KuaishouIndustryVoucherAdminListQuery, type KuaishouIndustryVoucherAdminListRow, } 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, isKuaishouIndustryEticketDestroyableStatus, KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS, resolveKuaishouIndustryEticketDisplayStatus, } from '../platforms/kuaishou-industry/check-available-service.js' import { getKuaishouIndustrySourceConfig, listKuaishouIndustryShopConfigs, type KuaishouIndustrySourceConfig, } from '../platforms/kuaishou-industry/source-config-service.js' import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js' import { consumeKuaishouIndustryVoucher, destroyKuaishouIndustryVoucher, resolveKuaishouIndustryEticketType, } 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 { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-industry/voucher-binding-service.js' import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js' export async function listAdminKuaishouIndustryVouchers( input: KuaishouIndustryVoucherAdminListQuery = {}, ) { const result = await listKuaishouIndustryVouchersForAdmin(input) const shopNameBySellerId = buildKuaishouIndustryShopNameMap() return { items: result.items.map((voucher) => mapAdminKuaishouIndustryVoucher(voucher, shopNameBySellerId), ), pagination: { page: result.page, pageSize: result.pageSize, total: result.total, }, } } /** 轻量店铺选项:仅展示名与 ID,供筛选/下拉使用(不含 token 等敏感字段) */ export function listAdminKuaishouIndustryShops() { const shops = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()) return { shops: shops .map((shop) => { const sellerId = String(shop.sellerId || '').trim() const shopId = String(shop.shopId || '').trim() const shopName = String(shop.customShopName || shop.shopName || '').trim() return { sellerId, shopId, shopName, enabled: shop.enabled !== false, } }) .filter((shop) => shop.sellerId || shop.shopId), } } export async function listAdminKuaishouIndustryRefunds(input: JsonObject = {}) { assertRequired(input.beginTime, '开始时间未填写') assertRequired(input.endTime, '结束时间未填写') const payload = await buildRefundOpenApiPayload(input) return mapOpenApiResult(await listKuaishouIndustryRefunds(payload)) } export async function approveAdminKuaishouIndustryRefund(input: JsonObject = {}) { assertRequired(input.refundId, '退款单编号未填写') assertRequired(input.refundAmount, '退款金额未填写') const payload = await buildRefundOpenApiPayload(input) return mapOpenApiResult(await approveKuaishouIndustryRefund(payload)) } export async function disagreeAdminKuaishouIndustryRefund(input: JsonObject = {}) { assertRequired(input.refundId, '退款单编号未填写') assertRequired(input.sellerDisagreeReason, '拒绝原因未填写') assertRequired(input.sellerDisagreeDesc, '拒绝说明未填写') assertRequired(input.status, '退款单当前状态未填写') assertRequired(input.negotiateStatus, '协商状态未填写') const payload = await buildRefundOpenApiPayload(input) return mapOpenApiResult(await disagreeKuaishouIndustryRefund(payload)) } 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, '电子凭证列表未填写') const result = mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload)) return { ...result, eticketType: String(payload.eticketType || payload.bizTypeCode || ''), voucher: voucher ? mapAdminKuaishouIndustryVoucher(voucher) : null, } } 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 consumeType = String(input.consumeType || '').trim() const consumeInput: Parameters[1] = { source: 'admin_tool_manual_consume', token: String(input.token || voucher.token || '').trim(), consumeTime: Date.now(), ...(consumeType ? { consumeType } : {}), ...(input.serialNum ? { serialNum: String(input.serialNum).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, } } export async function destroyAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) { const voucher = await resolveRequiredVoucher(input) const checkPayload = buildVoucherOpenApiPayload(input, voucher) assertRequired(checkPayload.sellerId, '卖家编号未填写,无法在销毁前查询卡券状态') assertRequired( checkPayload.eticketType || checkPayload.bizTypeCode, '电子凭证类型未填写,无法在销毁前查询卡券状态', ) assertEtickets(checkPayload.etickets, '电子凭证列表未填写,无法在销毁前查询卡券状态') const checkResult = await checkKuaishouIndustryEticketAvailable(checkPayload) if (!checkResult.success || checkResult.skippedReason) { const platformMessage = String( checkResult.error || checkResult.response?.error_msg || checkResult.response?.msg || '', ).trim() throw createHttpError( `销毁前查询卡券状态失败,已取消销毁${platformMessage ? `:${platformMessage}` : ''}`, { statusCode: 409, errorCode: 'admin_kuaishou_industry_destroy_status_check_failed', }, ) } const displayStatus = resolveKuaishouIndustryEticketDisplayStatus( checkResult.response, voucher.voucher_code, ) if (!isKuaishouIndustryEticketDestroyableStatus(displayStatus)) { throw createHttpError( displayStatus ? `卡券当前展示状态为 ${displayStatus},仅 ${KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS}(冻结)状态可以销毁` : `未获取到卡券展示状态,仅 ${KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS}(冻结)状态可以销毁`, { statusCode: 409, errorCode: 'admin_kuaishou_industry_voucher_not_frozen', }, ) } const task = voucher.task_id ? await getTaskById(voucher.task_id) : null const destroyInput: Parameters[1] = { source: 'admin_tool_manual_destroy', reason: String(input.reason || '').trim(), token: String(input.token || voucher.token || '').trim(), eticketType: resolveKuaishouIndustryEticketType(voucher, input.eticketType), } if (task) { destroyInput.task = task } const result = await destroyKuaishouIndustryVoucher(voucher, destroyInput) if (!result.ok || !result.voucher) { throw createHttpError(result.errorMessage || '电子凭证销毁失败', { statusCode: 409, errorCode: 'admin_kuaishou_industry_destroy_failed', }) } const nextTask = result.task || task if (nextTask && result.voucher && !result.alreadyDestroyed) { await attachKuaishouIndustryVoucherToTask(nextTask, result.voucher, { source: 'admin_tool_manual_destroy', now: new Date().toISOString(), }) } return { success: true, alreadyDestroyed: Boolean(result.alreadyDestroyed), reason: result.reason || '', voucher: mapAdminKuaishouIndustryVoucher(result.voucher), task: nextTask ? mapTaskReference(nextTask) : null, } } function buildVoucherOpenApiPayload( input: JsonObject, voucher: KuaishouIndustryVoucherRow | null, ): JsonObject { const etickets = normalizeAdminEtickets(input.etickets) const voucherCode = String(voucher?.voucher_code || input.voucherCode || '').trim() const eticketType = resolveKuaishouIndustryEticketType( voucher, input.eticketType || input.bizTypeCode, ) 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(), eticketType, ...(eticketType ? { bizTypeCode: eticketType } : {}), etickets: etickets.length > 0 ? etickets : voucherCode ? [{ id: voucherCode, code: voucherCode, num: 1 }] : [], } } async function buildRefundOpenApiPayload(input: JsonObject): Promise { const source = getKuaishouIndustrySourceConfig() const sellerId = String(input.sellerId || '').trim() || (await resolveRefundSellerIdFromLocalVoucher(input)) || resolveSingleEnabledKuaishouIndustrySellerId(source) if (!sellerId && hasMultipleEnabledKuaishouIndustryShops(source)) { throw createHttpError('存在多个快手售后退款店铺授权,请先选择 sellerId', { statusCode: 400, errorCode: 'admin_kuaishou_industry_refund_seller_id_required', }) } return { ...input, ...(sellerId ? { sellerId } : {}), } } async function resolveRefundSellerIdFromLocalVoucher(input: JsonObject): Promise { const voucherCode = String(input.voucherCode || '').trim() const oid = String(input.oid || input.orderId || '').trim() if (voucherCode) { const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, oid) return String(voucher?.seller_id || '').trim() } const taskId = Number(input.taskId || 0) if (Number.isFinite(taskId) && taskId > 0) { return resolveSingleSellerIdFromVouchers( await listKuaishouIndustryVouchersByTaskId(Math.trunc(taskId)), '当前任务关联多个卖家,请指定 sellerId', ) } if (oid) { return resolveSingleSellerIdFromVouchers( await listKuaishouIndustryVouchersByOid(oid), '当前订单关联多个卖家,请指定 sellerId', ) } return '' } function resolveSingleSellerIdFromVouchers( vouchers: KuaishouIndustryVoucherRow[], multipleMessage: string, ): string { const sellerIds = Array.from( new Set(vouchers.map((voucher) => String(voucher.seller_id || '').trim()).filter(Boolean)), ) if (sellerIds.length > 1) { throw createHttpError(multipleMessage, { statusCode: 409, errorCode: 'admin_kuaishou_industry_refund_seller_id_ambiguous', }) } return sellerIds[0] || '' } function resolveSingleEnabledKuaishouIndustrySellerId( source: KuaishouIndustrySourceConfig, ): string { const enabledShops = listEnabledKuaishouIndustryShops(source) return enabledShops.length === 1 ? String(enabledShops[0]?.sellerId || enabledShops[0]?.shopId || '').trim() : '' } function hasMultipleEnabledKuaishouIndustryShops(source: KuaishouIndustrySourceConfig): boolean { return listEnabledKuaishouIndustryShops(source).length > 1 } function listEnabledKuaishouIndustryShops(source: KuaishouIndustrySourceConfig) { return listKuaishouIndustryShopConfigs(source).filter( (shop) => shop.enabled !== false && String(shop.sellerId || shop.shopId || '').trim(), ) } async function resolveOptionalVoucher( input: JsonObject, ): Promise { 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 { 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 | KuaishouIndustryVoucherAdminListRow, shopNameBySellerId: Map = buildKuaishouIndustryShopNameMap(), ) { const sellerId = String(voucher.seller_id || '').trim() const listRow = voucher as KuaishouIndustryVoucherAdminListRow return { id: voucher.id, voucherCode: voucher.voucher_code, oid: voucher.oid, orderId: voucher.order_id, taskId: voucher.task_id, taskNo: String(listRow.task_no || '').trim(), unitIndex: voucher.unit_index, sellerId, shopName: shopNameBySellerId.get(sellerId) || '', skuCode: String(listRow.sku_code || '').trim(), skuName: String(listRow.sku_name || '').trim(), tokenMasked: maskSecret(voucher.token), eticketType: resolveKuaishouIndustryEticketType(voucher), 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 buildKuaishouIndustryShopNameMap(): Map { const map = new Map() const shops = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()) for (const shop of shops) { const name = String(shop.customShopName || shop.shopName || '').trim() if (!name) { continue } const sellerId = String(shop.sellerId || '').trim() const shopId = String(shop.shopId || '').trim() if (sellerId) { map.set(sellerId, name) } if (shopId && shopId !== sellerId) { map.set(shopId, name) } } return map } 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 asJsonObject(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 [] } }