import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js' import { resolveFulfillmentBinding, } from '../../repositories/fulfillment-profile-repo.js' import { createTaskClaimToken } from '../claim/claim-service.js' import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js' import { nowIso } from '../../utils/time.js' import { randomId } from '../../utils/random.js' import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' type JsonObject = Record type ClaimTokenLike = { token: string expired_at: string [key: string]: unknown } type FulfillmentBindingLike = { id: number profile_id?: number profile_key?: string profile_name?: string name?: string executor_key?: string requires_claim?: boolean auto_dispatch?: boolean config_json?: string | JsonObject [key: string]: unknown } type DeliveryTaskRow = TaskRow & { skuCode?: string skuName?: string } type DeliveryTaskDeps = { createTask?: typeof createTask listTasksByOrderId?: typeof listTasksByOrderId updateTask?: typeof updateTask resolveFulfillmentBinding?: (input: { skuCode: string provider?: string platform?: string shopId?: string }) => Promise createTaskClaimToken?: (taskId: number | string) => Promise notifyTaskAutoManualReview?: (payload: { task: unknown reason: string source: string }) => Promise | unknown nowIso?: () => string randomId?: (prefix?: string) => string } type RuntimeDeliveryTaskDeps = Required> type TaskContext = { [key: string]: unknown } export async function syncDeliveryTasksForOrder( order: OrderRow, orderItems: OrderItemRow[], ): Promise { return syncDeliveryTasksForOrderWithDeps(order, orderItems) } export async function syncDeliveryTasksForOrderWithDeps( order: OrderRow, orderItems: OrderItemRow[], deps: DeliveryTaskDeps = {}, ): Promise { const { createTask: createDeliveryTask = createTask, listTasksByOrderId: listTasks = listTasksByOrderId, updateTask: updateDeliveryTask = updateTask, resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding, createTaskClaimToken: createClaimToken = createTaskClaimToken, notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview, nowIso: getNowIso = nowIso, randomId: createRandomId = randomId, } = deps const runtimeDeps: RuntimeDeliveryTaskDeps = { updateTask: updateDeliveryTask, createTaskClaimToken: createClaimToken, notifyTaskAutoManualReview: notifyManualReview, nowIso: getNowIso, } const existingTasks = await listTasks(order.id) if (existingTasks.length > 0) { if (order.pay_status !== 'paid') { return existingTasks } const itemMap = new Map(orderItems.map((item) => [item.id, item])) const preparedTasks = await Promise.all(existingTasks.map((task) => preparePaidTask({ ...task, skuCode: itemMap.get(task.order_item_id)?.sku_code || '', skuName: itemMap.get(task.order_item_id)?.sku_name || '', }, runtimeDeps))) return preparedTasks.filter(isTaskRow) } const tasks: DeliveryTaskRow[] = [] for (const item of orderItems) { const binding = await resolveBinding({ skuCode: item.sku_code, provider: order.provider, platform: order.platform, shopId: order.shop_id, }) if (!binding) { continue } const profile = binding const quantity = Math.max(1, Number(item.quantity || 1)) const fulfillmentConfig = parseJsonObject(profile.config_json) const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles) const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume) const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop) for (let index = 0; index < quantity; index += 1) { const createdAt = getNowIso() const initialStatus = order.pay_status === 'paid' ? resolvePaidTaskStatus(profile) : 'pending_payment' const task = await createDeliveryTask({ orderId: order.id, orderItemId: item.id, unitIndex: index + 1, provider: order.provider, platform: order.platform, shopId: order.shop_id, shopName: order.shop_name, platformOrderId: order.platform_order_id, taskNo: createRandomId('DT'), profileId: Number(profile.profile_id || profile.id), executorKey: String(profile.executor_key || 'manual_dispatch'), taskStatus: initialStatus, deliveryStatus: 'pending', resultCode: '', resultMessage: '', claimToken: '', claimExpiresAt: null, automationMode: profile.auto_dispatch ? 'automatic' : 'manual', requiresClaim: Boolean(profile.requires_claim), userActionStatus: profile.requires_claim ? 'pending_claim' : 'not_required', attemptCount: 0, lastError: '', contextJson: JSON.stringify({ profileKey: String(profile.profile_key || ''), profileName: String(profile.profile_name || profile.name || ''), skuCode: item.sku_code, skuName: item.sku_name, kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key) ? { flowType: 'kuaishou_cloud_fulfillment', configId: String(fulfillmentConfig.configId || '').trim(), internalSkuCode: item.sku_code, internalSkuName: item.sku_name, ticket: { code: '', status: 'pending', capturedAt: null, capturedBy: null, verifiedAt: null, oid: '', formToken: '', leftCount: 0, goodsTitle: '', }, binding: { prepareStatus: 'pending', cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys), skuId: Number(cloudtentaclesConfig.skuId || 0) || 0, skuName: String(cloudtentaclesConfig.skuName || '').trim(), vnKey: '1', vnId: 0, vnPhone: '', bindUrl: '', bindPreparedAt: null, bindExpiresAt: null, bindProbeAt: null, bindProbeStatus: '', bindProbeMessage: '', }, role: { status: 'pending', name: '', rid: '', refreshedAt: null, errorMessage: '', rawInfo: null, }, purchase: { autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false, minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0, usedKnapsack: false, purchaseTriggered: false, assetBefore: 0, assetAfter: 0, purchaseAt: null, }, dispatch: { status: 'pending', dispatchAt: null, dispatchBy: null, sendType: 0, note: '', }, returnNumber: { status: 'pending', returnedAt: null, returnedBy: null, autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true, }, consume: { status: 'pending', shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || '').trim(), shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(), autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true, consumedAt: null, errorMessage: '', }, notes: String(fulfillmentConfig.notes || '').trim(), } : null, }), createdAt, updatedAt: createdAt, }) if (task) { tasks.push({ ...task, skuCode: item.sku_code, skuName: item.sku_name, }) } } } if (order.pay_status !== 'paid') { return tasks } const preparedTasks = await Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps))) return preparedTasks.filter(isTaskRow) } async function preparePaidTask( task: DeliveryTaskRow, deps: Partial = {}, ): Promise { const { updateTask: updateDeliveryTask = updateTask, createTaskClaimToken: createClaimToken = createTaskClaimToken, notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview, nowIso: getNowIso = nowIso, } = deps const now = getNowIso() if (['link_generated', 'claimed', 'role_confirmed', 'redeeming', 'redeemed', 'closed'].includes(task.task_status)) { return task } if (isKuaishouCloudExecutor(task.executor_key)) { if ([ 'pending_binding_prepare', 'waiting_binding', 'dispatched_pending_return', 'completed', 'manual_review', 'failed', ].includes(String(task.task_status || '').trim())) { if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) { const claimToken = await createClaimToken(task.id) return updateDeliveryTask(task.id, { claim_token: claimToken.token, claim_expires_at: claimToken.expired_at, user_action_status: 'pending_claim', updated_at: now, }) } return task } const claimToken = await createClaimToken(task.id) return updateDeliveryTask(task.id, { task_status: 'pending_binding_prepare', claim_token: claimToken.token, claim_expires_at: claimToken.expired_at, user_action_status: 'pending_claim', last_error: task.last_error || '领取链接已生成,等待客户提交核销码', updated_at: now, }) } if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') { const lastError = task.last_error || '当前任务需要人工履约处理' const updatedTask = await updateDeliveryTask(task.id, { task_status: 'manual_review', user_action_status: 'not_required', last_error: lastError, updated_at: now, }) await notifyManualReview({ task: updatedTask || task, reason: lastError, source: 'manual_dispatch_profile', }) return updatedTask } return task } function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string { if (Boolean(profile?.requires_claim)) { return 'paid' } if (isKuaishouCloudExecutor(profile?.executor_key)) { return 'pending_binding_prepare' } if (String(profile?.executor_key || '').trim() === 'manual_dispatch') { return 'manual_review' } return 'paid' } function parseTaskContext(task: { context_json?: unknown } | null | undefined): TaskContext { const value = task?.context_json if (!value) { return {} } if (typeof value === 'object') { return value as TaskContext } try { const parsed = JSON.parse(String(value || '{}')) return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as TaskContext : {} } catch { return {} } } function parseJsonObject(value: unknown): JsonObject { if (!value) { return {} } if (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 as JsonObject : {} } catch { return {} } } function normalizeStringArray(value: unknown): string[] { if (!Array.isArray(value)) { return [] } return Array.from( new Set(value.map((item) => String(item || '').trim()).filter(Boolean)), ) } function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow { return Boolean(task && Number(task.id || 0) > 0) } function isKuaishouCloudExecutor(value: unknown): boolean { return String(value || '').trim() === 'kuaishou_ct_assisted' }