diff --git a/apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts b/apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts index e7145258..82d54bef 100644 --- a/apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts +++ b/apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts @@ -9,6 +9,9 @@ import { } from './types.js' import type { TaskRow } from '../../../types/repository/rows.js' +/** + * @deprecated 待移除,快手飞飞直充渠道已废弃,不再使用。 + */ export const kuaishouFeifeiExecutor: FulfillmentExecutor = { key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI, preparePaidTask, diff --git a/apps/backend/src/services/fulfillment/executors/manual-executor.ts b/apps/backend/src/services/fulfillment/executors/manual-executor.ts index a93bf178..f4146253 100644 --- a/apps/backend/src/services/fulfillment/executors/manual-executor.ts +++ b/apps/backend/src/services/fulfillment/executors/manual-executor.ts @@ -1,4 +1,5 @@ import { TASK_STATUS } from '../../../domain/task-status.js' +import { autoConsumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-auto-consume-service.js' import { FULFILLMENT_EXECUTOR_KEYS, type FulfillmentExecutor, @@ -29,5 +30,12 @@ async function preparePaidTask( source: 'manual_dispatch_profile', }) + const effectiveTask = updatedTask || task + await autoConsumeKuaishouIndustryVouchersForTask(effectiveTask, { + source: 'manual_dispatch_prepared', + }).catch(() => { + // 忽略异常,发码回调完成后仍会自动补核销 + }) + return updatedTask } diff --git a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts index c44dfe25..31ae6801 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts @@ -18,6 +18,9 @@ import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-in import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js' import type { TaskRow } from '../../../types/repository/rows.js' +/** + * @deprecated 待移除,快手飞飞直充渠道已废弃,不再使用。 + */ export function isKuaishouFeifeiTask(task: Partial | null | undefined) { return String(task?.executor_key || '').trim() === 'kuaishou_feifei' } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts index 192f1462..3d8d13b0 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts @@ -11,10 +11,7 @@ import { buildIndustryErrorResponse, buildIndustryQueryCodeData, } from './response.js' -import { - buildKuaishouIndustryEticketFromVoucher, - isKuaishouIndustryVoucherSendCallbackSuccess, -} from './voucher-service.js' +import { buildKuaishouIndustryEticketFromVoucher } from './voucher-service.js' export async function handleQueryCode(rawBody: JsonObject = {}) { const config = getKuaishouIndustryConfig() @@ -32,9 +29,6 @@ export async function handleQueryCode(rawBody: JsonObject = {}) { if (!matched) { return buildIndustryErrorResponse(4012005, `卡券不存在: ${params.eticketId}`) } - if (!isKuaishouIndustryVoucherSendCallbackSuccess(matched)) { - return buildIndustryErrorResponse(4012005, `卡券尚未完成发码: ${params.eticketId}`) - } const eticket = buildKuaishouIndustryEticketFromVoucher(matched, params.eticketType) return buildIndustrySuccessResponse( @@ -47,9 +41,7 @@ export async function handleQueryCode(rawBody: JsonObject = {}) { ) } - const vouchers = (await listKuaishouIndustryVouchersByOid(normalizedOid)).filter( - isKuaishouIndustryVoucherSendCallbackSuccess, - ) + const vouchers = await listKuaishouIndustryVouchersByOid(normalizedOid) if (vouchers.length === 0) { return buildIndustryErrorResponse(4012002, `订单不存在: ${normalizedOid}`) } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 253e4d40..d899de85 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -30,6 +30,7 @@ import { resolveKuaishouIndustryVoucherValidity, } from './voucher-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js' +import { autoConsumeKuaishouIndustryVouchersForOrder } from './voucher-auto-consume-service.js' import { parseAmountToFen } from '../../../utils/money.js' import { publishAdminOrderRealtimeChange, @@ -503,20 +504,45 @@ async function syncOpen91OrderAfterSendCallbackSuccess( now: normalizeTimestampIso(new Date().toISOString()), }) } - if (isOpen91OrderAwaitingFulfillmentConfig(error)) { - return + if (!isOpen91OrderAwaitingFulfillmentConfig(error)) { + logIntegration( + '[kuaishou-industry/send-code]', + '电子凭证发货回调成功后重试 91 订单失败', + { + source, + oid: normalizedOid, + orderId: order.id, + error: error instanceof Error ? error.message : String(error), + }, + { level: 'warn' }, + ) } - logIntegration( - '[kuaishou-industry/send-code]', - '电子凭证发货回调成功后重试 91 订单失败', - { + } + + try { + const autoConsumeResult = await autoConsumeKuaishouIndustryVouchersForOrder(normalizedOid, { + source: `${source}_auto_consume`, + }) + if (autoConsumeResult.consumedCount > 0 || autoConsumeResult.failedCount > 0) { + logIntegration('[kuaishou-industry/send-code]', '发码回调成功后自动推进电子凭证核销完成', { source, oid: normalizedOid, orderId: order.id, - error: error instanceof Error ? error.message : String(error), - }, - { level: 'warn' }, - ) + consumedCount: autoConsumeResult.consumedCount, + alreadyConsumedCount: autoConsumeResult.alreadyConsumedCount, + skippedCount: autoConsumeResult.skippedCount, + failedCount: autoConsumeResult.failedCount, + skippedReasons: autoConsumeResult.skippedReasons, + errors: autoConsumeResult.errors, + }) + } + } catch (consumeError) { + logWarn('[kuaishou-industry/send-code]', '发码回调成功后自动推进电子凭证核销异常', { + source, + oid: normalizedOid, + orderId: order.id, + error: consumeError instanceof Error ? consumeError.message : String(consumeError), + }) } } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.test.ts new file mode 100644 index 00000000..58c199be --- /dev/null +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.test.ts @@ -0,0 +1,262 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + autoConsumeKuaishouIndustryVouchersForOrder, + autoConsumeKuaishouIndustryVouchersForTask, +} from './voucher-auto-consume-service.js' +import { WORK_ORDER_STATUS } from '../../../domain/work-order-status.js' +import type { + KuaishouIndustryVoucherRow, + OrderRow, + TaskRow, +} from '../../../types/repository/rows.js' +import type { WorkOrderRow } from '../../../repositories/worker-platform/index.js' + +function buildMockVoucher( + overrides: Partial = {}, +): KuaishouIndustryVoucherRow { + return { + id: 1, + voucher_code: 'KSV001', + oid: '26250000000001', + order_id: 100, + task_id: 200, + unit_index: 1, + seller_id: '4269276762', + token: 'token-1', + eticket_type: 'GAME_OPEN_TICKET_CONSUME', + status: 'UNUSED', + valid_start_time: 1789000000000, + valid_end_time: 1791592000000, + consume_serial_num: '', + consume_details_json: '[]', + consumed_at: null, + destroyed_at: null, + raw_payload_json: '{}', + send_callback_status: 'success', + send_callback_attempt_count: 1, + send_callback_last_error: '', + send_callback_response_json: '{}', + send_callback_sent_at: '2026-09-10T09:00:00.000Z', + created_at: '2026-09-10T09:00:00.000Z', + updated_at: '2026-09-10T09:00:00.000Z', + ...overrides, + } +} + +function buildMockTask(overrides: Partial = {}): TaskRow { + return { + id: 200, + order_id: 100, + order_item_id: 300, + provider: 'open_91', + platform: '91kaquan', + shop_id: '4269276762', + platform_order_id: '26250000000001', + task_no: 'TASK-001', + unit_index: 1, + task_status: 'manual_review', + delivery_status: 'pending', + user_action_status: 'not_required', + executor_key: 'manual_dispatch', + sku_code: 'SKU001', + sku_name: '测试商品', + quantity: 1, + requires_claim: false, + auto_dispatch: false, + claim_token: '', + claim_expires_at: null, + primary_claim_token_id: null, + primary_claim_token: '', + primary_claim_expires_at: null, + last_error: '', + result_code: '', + result_message: '', + context_json: '{}', + claimed_at: null, + role_confirmed_at: null, + redeemed_at: null, + created_at: '2026-09-10T09:00:00.000Z', + updated_at: '2026-09-10T09:00:00.000Z', + ...overrides, + } +} + +function buildMockWorkOrder(overrides: Partial = {}): WorkOrderRow { + return { + id: 500, + work_order_no: 'WO-001', + order_id: 100, + order_item_id: 300, + task_id: 200, + source: 'test', + status: WORK_ORDER_STATUS.UNASSIGNED, + assigned_worker_id: null, + assigned_at: null, + completed_at: null, + reward_amount: 1000, + refund_amount: 0, + platform_order_id: '26250000000001', + product_name: '测试工单', + material_json: '{}', + created_at: '2026-09-10T09:00:00.000Z', + updated_at: '2026-09-10T09:00:00.000Z', + ...overrides, + } as unknown as WorkOrderRow +} + +test('缺少 oid 时直接返回 skipped', async () => { + const result = await autoConsumeKuaishouIndustryVouchersForOrder('') + assert.equal(result.skippedCount, 1) + assert.deepEqual(result.skippedReasons, ['missing_oid']) +}) + +test('未找到凭证时直接返回 skipped', async () => { + const result = await autoConsumeKuaishouIndustryVouchersForOrder( + '26250000000001', + {}, + { + listVouchersByOid: async () => [], + }, + ) + assert.equal(result.voucherCount, 0) + assert.equal(result.skippedCount, 1) + assert.deepEqual(result.skippedReasons, ['voucher_not_found']) +}) + +test('已核销的凭证不重复核销,跳过并记录 already_consumed', async () => { + const voucher = buildMockVoucher({ status: 'CONSUMED' }) + let consumeCalled = false + + const result = await autoConsumeKuaishouIndustryVouchersForOrder( + '26250000000001', + {}, + { + listVouchersByOid: async () => [voucher], + consumeVoucher: async () => { + consumeCalled = true + return { ok: true, voucher, callbackSuccess: true } + }, + }, + ) + + assert.equal(consumeCalled, false) + assert.equal(result.alreadyConsumedCount, 1) + assert.equal(result.skippedCount, 1) + assert.match(result.skippedReasons[0] || '', /already_consumed/) +}) + +test('发码出站回调未确认时(pending 或 failed),安全跳过不触发核销', async () => { + const voucher = buildMockVoucher({ send_callback_status: 'pending' }) + let consumeCalled = false + + const result = await autoConsumeKuaishouIndustryVouchersForOrder( + '26250000000001', + {}, + { + listVouchersByOid: async () => [voucher], + consumeVoucher: async () => { + consumeCalled = true + return { ok: true, voucher, callbackSuccess: true } + }, + }, + ) + + assert.equal(consumeCalled, false) + assert.equal(result.consumedCount, 0) + assert.equal(result.skippedCount, 1) + assert.match(result.skippedReasons[0] || '', /send_callback_unconfirmed/) +}) + +test('发单平台待发布大厅工单(unassigned/pending_material):跳过自动核销,保留在发布大厅时核销', async () => { + const voucher = buildMockVoucher({ task_id: 200 }) + const task = buildMockTask({ id: 200 }) + const workOrder = buildMockWorkOrder({ task_id: 200, status: WORK_ORDER_STATUS.UNASSIGNED }) + let consumeCalled = false + + const result = await autoConsumeKuaishouIndustryVouchersForOrder( + '26250000000001', + {}, + { + listVouchersByOid: async () => [voucher], + findOrder: async () => ({ id: 100 }) as OrderRow, + listTasks: async () => [task], + listWorkOrders: async () => [workOrder], + consumeVoucher: async () => { + consumeCalled = true + return { ok: true, voucher, callbackSuccess: true } + }, + }, + ) + + assert.equal(consumeCalled, false) + assert.equal(result.consumedCount, 0) + assert.equal(result.skippedCount, 1) + assert.match(result.skippedReasons[0] || '', /waiting_work_order_hall_publish/) +}) + +test('常规非大厅订单(人工派单/其他直接任务):发码成功后立即自动核销', async () => { + const voucher = buildMockVoucher({ task_id: 200, send_callback_status: 'success' }) + const task = buildMockTask({ id: 200, executor_key: 'manual_dispatch' }) + let consumeCalled = false + let attachCalled = false + + const result = await autoConsumeKuaishouIndustryVouchersForOrder( + '26250000000001', + { source: 'test_auto_consume' }, + { + listVouchersByOid: async () => [voucher], + findOrder: async () => ({ id: 100 }) as OrderRow, + listTasks: async () => [task], + listWorkOrders: async () => [], // 无发单平台待发布工单 + consumeVoucher: async (v, input) => { + consumeCalled = true + assert.equal(input.source, 'test_auto_consume') + return { + ok: true, + voucher: { ...v, status: 'CONSUMED' }, + callbackSuccess: true, + } + }, + attachVoucher: async () => { + attachCalled = true + return task + }, + }, + ) + + assert.equal(consumeCalled, true) + assert.equal(attachCalled, true) + assert.equal(result.consumedCount, 1) + assert.equal(result.failedCount, 0) +}) + +test('autoConsumeKuaishouIndustryVouchersForTask 正确提取任务 platform_order_id 并推进', async () => { + const task = buildMockTask({ platform_order_id: '26250000000001' }) + const voucher = buildMockVoucher({ task_id: 200, send_callback_status: 'success' }) + + let consumeCalled = false + const result = await autoConsumeKuaishouIndustryVouchersForTask( + task, + { source: 'task_test' }, + { + listVouchersByOid: async () => [voucher], + findOrder: async () => ({ id: 100 }) as OrderRow, + listTasks: async () => [task], + listWorkOrders: async () => [], + consumeVoucher: async (v) => { + consumeCalled = true + return { + ok: true, + voucher: { ...v, status: 'CONSUMED' }, + callbackSuccess: true, + } + }, + attachVoucher: async () => task, + }, + ) + + assert.equal(consumeCalled, true) + assert.equal(result.consumedCount, 1) +}) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.ts new file mode 100644 index 00000000..aff30e98 --- /dev/null +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-auto-consume-service.ts @@ -0,0 +1,286 @@ +import { listKuaishouIndustryVouchersByOid } from '../../../repositories/kuaishou-industry-voucher-repo.js' +import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js' +import { listTasksByOrderId } from '../../../repositories/task-repo.js' +import { listWorkOrdersByPlatformOrderId } from '../../../repositories/worker-platform/work-order-query-repo.js' +import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' +import { + consumeKuaishouIndustryVoucher, + isKuaishouIndustryVoucherSendCallbackSuccess, + normalizeVoucherStatus, +} from './voucher-service.js' +import { logIntegration, logWarn } from '../../../utils/logger.js' +import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js' +import { normalizeWorkOrderStatus, WORK_ORDER_STATUS } from '../../../domain/work-order-status.js' +import type { + KuaishouIndustryVoucherRow, + OrderRow, + TaskRow, +} from '../../../types/repository/rows.js' +import type { WorkOrderRow } from '../../../repositories/worker-platform/index.js' + +export type VoucherAutoConsumeResult = { + oid: string + voucherCount: number + consumedCount: number + alreadyConsumedCount: number + skippedCount: number + failedCount: number + skippedReasons: string[] + errors: string[] +} + +type VoucherAutoConsumeDeps = { + listVouchersByOid?: typeof listKuaishouIndustryVouchersByOid + findOrder?: typeof findLatestOrderByPlatformOrderId + listTasks?: typeof listTasksByOrderId + listWorkOrders?: typeof listWorkOrdersByPlatformOrderId + consumeVoucher?: typeof consumeKuaishouIndustryVoucher + attachVoucher?: typeof attachKuaishouIndustryVoucherToTask +} + +/** + * 自动推进快手电子凭证核销: + * + * 规则: + * 1. 凭证必须已完成发码出站回调(send_callback_status = success),否则快手开放平台不可核销; + * 2. 路由到发单平台的工单:如果工单处于待发布大厅/待派单状态(pending_material / unassigned), + * 严格跳过,保留在「发布到大厅 / 指派接单员」时由 publish-voucher-service 触发核销; + * 3. 其他所有订单(人工派单 manual_dispatch、分销代充 affiliate_dash、已在工单大厅等其他常规任务): + * 在任务生成或发码确认后,立即执行出站核销并回写任务上下文,消除买家未消费极速退款窗口。 + */ +export async function autoConsumeKuaishouIndustryVouchersForOrder( + oid: string, + options: { source?: string } = {}, + deps: VoucherAutoConsumeDeps = {}, +): Promise { + const normalizedOid = String(oid || '').trim() + const source = String(options.source || 'auto_consume').trim() || 'auto_consume' + + const result: VoucherAutoConsumeResult = { + oid: normalizedOid, + voucherCount: 0, + consumedCount: 0, + alreadyConsumedCount: 0, + skippedCount: 0, + failedCount: 0, + skippedReasons: [], + errors: [], + } + + if (!normalizedOid) { + result.skippedCount += 1 + result.skippedReasons.push('missing_oid') + return result + } + + const listVouchers = deps.listVouchersByOid || listKuaishouIndustryVouchersByOid + const findOrder = deps.findOrder || findLatestOrderByPlatformOrderId + const listTasks = deps.listTasks || listTasksByOrderId + const listWorkOrders = deps.listWorkOrders || listWorkOrdersByPlatformOrderId + const consumeVoucher = deps.consumeVoucher || consumeKuaishouIndustryVoucher + const attachVoucher = deps.attachVoucher || attachKuaishouIndustryVoucherToTask + + const vouchers = await listVouchers(normalizedOid) + result.voucherCount = vouchers.length + if (vouchers.length === 0) { + result.skippedCount += 1 + result.skippedReasons.push('voucher_not_found') + return result + } + + const candidateVouchers: KuaishouIndustryVoucherRow[] = [] + for (const voucher of vouchers) { + const voucherCode = String(voucher.voucher_code || '').trim() + const status = normalizeVoucherStatus(voucher.status) + + if (status === 'CONSUMED') { + result.alreadyConsumedCount += 1 + result.skippedCount += 1 + result.skippedReasons.push(`voucher_${voucherCode}:already_consumed`) + continue + } + + if (status === 'DESTROYED') { + result.skippedCount += 1 + result.skippedReasons.push(`voucher_${voucherCode}:already_destroyed`) + continue + } + + if (!isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) { + result.skippedCount += 1 + result.skippedReasons.push(`voucher_${voucherCode}:send_callback_unconfirmed`) + continue + } + + candidateVouchers.push(voucher) + } + + if (candidateVouchers.length === 0) { + return result + } + + const [order, workOrders] = await Promise.all([ + findOrder({ + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + platformOrderId: normalizedOid, + }), + listWorkOrders(normalizedOid), + ]) + + const tasks = order ? await listTasks(order.id) : [] + + for (const voucher of candidateVouchers) { + const voucherCode = String(voucher.voucher_code || '').trim() + const matchedTask = resolveTaskForVoucher(voucher, tasks) + const matchedWorkOrder = resolveWorkOrderForVoucher(voucher, matchedTask, workOrders) + + // 发单平台工单:待发布到大厅或待补充资料时,不提前核销,等待发布大厅动作 + if (matchedWorkOrder && isWorkOrderWaitingHallPublish(matchedWorkOrder)) { + result.skippedCount += 1 + result.skippedReasons.push( + `voucher_${voucherCode}:waiting_work_order_hall_publish(wo_${matchedWorkOrder.id})`, + ) + continue + } + + try { + const consumeRes = await consumeVoucher(voucher, { + source, + consumeTime: Date.now(), + ...(matchedTask ? { task: matchedTask } : {}), + }) + + if (!consumeRes.ok || !consumeRes.voucher) { + result.failedCount += 1 + const errMsg = consumeRes.errorMessage || `电子凭证 ${voucherCode} 核销失败` + result.errors.push(errMsg) + logWarn('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销失败`, { + oid: normalizedOid, + voucherCode, + source, + error: errMsg, + }) + continue + } + + result.consumedCount += 1 + + if (matchedTask) { + await attachVoucher(matchedTask, consumeRes.voucher, { + source, + now: new Date().toISOString(), + }).catch((err) => { + logWarn('[kuaishou-industry/auto-consume]', '核销后回写任务上下文异常', { + oid: normalizedOid, + voucherCode, + taskId: matchedTask.id, + error: err instanceof Error ? err.message : String(err), + }) + }) + } + + logIntegration('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销成功`, { + oid: normalizedOid, + voucherCode, + source, + taskId: matchedTask?.id || null, + }) + } catch (error) { + result.failedCount += 1 + const errMsg = error instanceof Error ? error.message : String(error) + result.errors.push(errMsg) + logWarn('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销异常`, { + oid: normalizedOid, + voucherCode, + source, + error: errMsg, + }) + } + } + + return result +} + +/** + * 针对指定履约任务执行电子凭证自动核销。 + */ +export async function autoConsumeKuaishouIndustryVouchersForTask( + task: TaskRow, + options: { source?: string } = {}, + deps: VoucherAutoConsumeDeps = {}, +): Promise { + const oid = String(task.platform_order_id || '').trim() + if (!oid) { + return { + oid: '', + voucherCount: 0, + consumedCount: 0, + alreadyConsumedCount: 0, + skippedCount: 1, + failedCount: 0, + skippedReasons: ['task_missing_platform_order_id'], + errors: [], + } + } + + return autoConsumeKuaishouIndustryVouchersForOrder( + oid, + { source: options.source || 'task_prepared_auto_consume' }, + deps, + ) +} + +function resolveTaskForVoucher( + voucher: KuaishouIndustryVoucherRow, + tasks: TaskRow[], +): TaskRow | null { + const voucherTaskId = Number(voucher.task_id || 0) + if (voucherTaskId > 0) { + const byId = tasks.find((t) => Number(t.id) === voucherTaskId) + if (byId) return byId + } + + const unitIndex = Number(voucher.unit_index || 0) + if (unitIndex > 0) { + const byUnit = tasks.find((t) => Number(t.unit_index || 0) === unitIndex) + if (byUnit) return byUnit + } + + return tasks[0] || null +} + +function resolveWorkOrderForVoucher( + voucher: KuaishouIndustryVoucherRow, + task: TaskRow | null, + workOrders: WorkOrderRow[], +): WorkOrderRow | null { + if (workOrders.length === 0) { + return null + } + + const voucherTaskId = Number(voucher.task_id || 0) + if (voucherTaskId > 0) { + const byTaskId = workOrders.find((wo) => Number(wo.task_id || 0) === voucherTaskId) + if (byTaskId) return byTaskId + } + + if (task) { + const taskId = Number(task.id || 0) + const byTask = workOrders.find((wo) => Number(wo.task_id || 0) === taskId) + if (byTask) return byTask + + const orderItemId = Number(task.order_item_id || 0) + if (orderItemId > 0) { + const byItem = workOrders.find((wo) => Number(wo.order_item_id || 0) === orderItemId) + if (byItem) return byItem + } + } + + return null +} + +function isWorkOrderWaitingHallPublish(workOrder: WorkOrderRow): boolean { + const status = normalizeWorkOrderStatus(workOrder.status) + return status === WORK_ORDER_STATUS.PENDING_MATERIAL || status === WORK_ORDER_STATUS.UNASSIGNED +} diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts index 746ace18..a659a5ba 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts @@ -497,7 +497,7 @@ function appendConsumeDetail(details: JsonObject[], nextDetail: JsonObject): Jso return [...details, nextDetail] } -function normalizeVoucherStatus(value: unknown): string { +export function normalizeVoucherStatus(value: unknown): string { const normalized = String(value || '') .trim() .toUpperCase()