import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js' import { listFulfillmentProfileRequirements, resolveFulfillmentBinding, } from '../../repositories/fulfillment-profile-repo.js' import { reserveInventoryForTask } from './inventory-service.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 { InventoryItemRow, 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 FulfillmentRequirementLike = { role_key?: string roleKey?: string credential_type?: string credentialType?: string is_required?: boolean [key: string]: unknown } type DeliveryTaskRow = TaskRow & { skuCode?: string skuName?: string } type DeliveryTaskDeps = { createTask?: typeof createTask listTasksByOrderId?: typeof listTasksByOrderId updateTask?: typeof updateTask listFulfillmentProfileRequirements?: ( profileId: number | string, ) => Promise resolveFulfillmentBinding?: (input: { skuCode: string provider?: string platform?: string shopId?: string }) => Promise reserveInventoryForTask?: (input: { skuCode: string taskId: number | string credentialType?: string roleKey?: 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 = { inventorySkuCode?: string primaryRequirement?: { roleKey?: string credentialType?: string } | null [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, listFulfillmentProfileRequirements: listProfileRequirements = listFulfillmentProfileRequirements, resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding, reserveInventoryForTask: reserveInventory = reserveInventoryForTask, createTaskClaimToken: createClaimToken = createTaskClaimToken, notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview, nowIso: getNowIso = nowIso, randomId: createRandomId = randomId, } = deps const runtimeDeps: RuntimeDeliveryTaskDeps = { updateTask: updateDeliveryTask, reserveInventoryForTask: reserveInventory, 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])) return 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))) } 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 requirements = await listProfileRequirements(profile.profile_id || profile.id) const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null 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, inventoryStatus: profile.requires_claim ? 'pending' : 'not_required', 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, inventorySkuCode: item.sku_code, primaryRequirement: primaryRequirement ? { roleKey: String(primaryRequirement.role_key || primaryRequirement.roleKey || 'primary_code'), credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'tencent_code'), } : null, 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', cloudSourceKey: String(cloudtentaclesConfig.cloudSourceKey || 'default').trim() || 'default', 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, }) tasks.push({ ...task, skuCode: item.sku_code, skuName: item.sku_name, }) } } if (order.pay_status !== 'paid') { return tasks } return Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps))) } async function preparePaidTask( task: DeliveryTaskRow, deps: Partial = {}, ): Promise { const { updateTask: updateDeliveryTask = updateTask, reserveInventoryForTask: reserveInventory = reserveInventoryForTask, createTaskClaimToken: createClaimToken = createTaskClaimToken, notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview, nowIso: getNowIso = nowIso, } = deps const now = getNowIso() const taskContext = parseTaskContext(task) const primaryRequirement = taskContext.primaryRequirement || null const inventorySkuCode = String(taskContext.inventorySkuCode || task.skuCode || '').trim() 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', inventory_status: 'not_required', 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', inventory_status: 'not_required', user_action_status: 'not_required', last_error: lastError, updated_at: now, }) await notifyManualReview({ task: updatedTask || task, reason: lastError, source: 'manual_dispatch_profile', }) return updatedTask } if (!inventorySkuCode) { const lastError = '未匹配到 SKU,无法为任务分配库存凭据' const updatedTask = await updateDeliveryTask(task.id, { task_status: 'manual_review', last_error: lastError, updated_at: now, }) await notifyManualReview({ task: updatedTask || task, reason: lastError, source: 'missing_sku', }) return updatedTask } if (task.primary_inventory_item_id && task.primary_claim_token_id) { return updateDeliveryTask(task.id, { task_status: 'link_generated', last_error: '', updated_at: now, }) } if (!primaryRequirement?.credentialType) { const lastError = '履约档案未配置库存要求,无法自动分配库存' const updatedTask = await updateDeliveryTask(task.id, { task_status: 'manual_review', last_error: lastError, updated_at: now, }) await notifyManualReview({ task: updatedTask || task, reason: lastError, source: 'missing_inventory_requirement', }) return updatedTask } const reserved = await reserveInventory({ skuCode: inventorySkuCode, taskId: task.id, credentialType: primaryRequirement.credentialType, roleKey: primaryRequirement.roleKey || 'primary_code', }) if (!reserved) { return updateDeliveryTask(task.id, { task_status: 'waiting_inventory', inventory_status: 'pending', last_error: '库存不足,等待可用库存凭据', updated_at: now, }) } const claimToken = await createClaimToken(task.id) return updateDeliveryTask(task.id, { task_status: 'link_generated', inventory_status: 'reserved', claim_token: claimToken.token, claim_expires_at: claimToken.expired_at, user_action_status: 'pending_claim', last_error: '', updated_at: now, }) } 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 isKuaishouCloudExecutor(value: unknown): boolean { return String(value || '').trim() === 'kuaishou_ct_assisted' }