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 { nowIso } from '../../utils/time.js' import { randomId } from '../../utils/random.js' export async function syncDeliveryTasksForOrder(order, orderItems) { const existingTasks = await listTasksByOrderId(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 || '', }))) } const tasks = [] for (const item of orderItems) { const binding = await resolveFulfillmentBinding({ skuCode: item.sku_code, provider: order.provider, platform: order.platform, shopId: order.shop_id, }) if (!binding) { continue } const profile = binding const requirements = await listFulfillmentProfileRequirements(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) for (let index = 0; index < quantity; index += 1) { const createdAt = nowIso() const initialStatus = order.pay_status === 'paid' ? resolvePaidTaskStatus(profile) : 'pending_payment' const task = await createTask({ 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: randomId('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: initialStatus === 'redeemed' ? 'delivered' : '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: '', capturedAt: null, capturedBy: null, }, binding: { prepareStatus: 'pending', cloudSourceKey: String(fulfillmentConfig.cloudtentacles?.cloudSourceKey || 'default').trim() || 'default', skuId: Number(fulfillmentConfig.cloudtentacles?.skuId || 0) || 0, skuName: String(fulfillmentConfig.cloudtentacles?.skuName || '').trim(), vnKey: '1', vnId: 0, vnPhone: '', bindUrl: '', bindPreparedAt: null, }, purchase: { autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false, minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.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: fulfillmentConfig.cloudtentacles?.autoReturnNumberAfterDispatch === true, }, consume: { status: 'pending', shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || '').trim(), autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true, }, 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))) } async function preparePaidTask(task) { const now = nowIso() 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())) { return task } return updateTask(task.id, { task_status: 'pending_binding_prepare', inventory_status: 'not_required', user_action_status: 'pending', last_error: task.last_error || '等待准备绑定资源', updated_at: now, }) } if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') { return updateTask(task.id, { task_status: 'manual_review', inventory_status: 'not_required', user_action_status: 'not_required', last_error: task.last_error || '当前任务需要人工履约处理', updated_at: now, }) } if (!inventorySkuCode) { return updateTask(task.id, { task_status: 'manual_review', last_error: '未匹配到 SKU,无法为任务分配库存凭据', updated_at: now, }) } if (task.primary_inventory_item_id && task.primary_claim_token_id) { return updateTask(task.id, { task_status: 'link_generated', last_error: '', updated_at: now, }) } if (!primaryRequirement?.credentialType) { return updateTask(task.id, { task_status: 'manual_review', last_error: '履约档案未配置库存要求,无法自动分配库存', updated_at: now, }) } const reserved = await reserveInventoryForTask({ skuCode: inventorySkuCode, taskId: task.id, credentialType: primaryRequirement.credentialType, roleKey: primaryRequirement.roleKey || 'primary_code', }) if (!reserved) { return updateTask(task.id, { task_status: 'waiting_inventory', inventory_status: 'pending', last_error: '库存不足,等待可用库存凭据', updated_at: now, }) } const claimToken = await createTaskClaimToken(task.id) return updateTask(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) { 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) { const value = task?.context_json if (!value) { return {} } if (typeof value === 'object') { return value } try { return JSON.parse(String(value || '{}')) } catch { return {} } } function parseJsonObject(value) { if (!value) { return {} } if (typeof value === 'object' && !Array.isArray(value)) { return value } try { const parsed = JSON.parse(String(value || '{}')) return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} } catch { return {} } } function isKuaishouCloudExecutor(value) { return String(value || '').trim() === 'kuaishou_ct_assisted' }