const MODULE = 'phase4-overseas-pda' const SCENARIO = 'phase4-overseas-pda-inbound-lifecycle' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function longId(value) { return /^\d+$/.test(id(value)) && id(value) !== '0' } function nonBlank(value) { return typeof value === 'string' && value.trim().length > 0 } function records(page) { return Array.isArray(page?.records) ? page.records : [] } function query(path, params) { const search = new URLSearchParams() for (const [key, value] of Object.entries(params)) if (value !== undefined && value !== null && value !== '') search.set(key, String(value)) return `${path}?${search}` } function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function key(context, action, suffix = '') { return `${context.marker}-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120) } async function domesticGet(context, name, path) { const response = await context.request('GET', path) const passed = success(response) context.addStep(MODULE, name, response, passed, trace(response), OPTIONS) return passed ? response.body.data : null } async function overseas(context, method, name, path, body = undefined) { const response = await context.requestOverseas(method, path, body) const passed = success(response) context.addStep(MODULE, name, response, passed, trace(response), OPTIONS) return { response, data: passed ? response.body.data : null } } async function findUnplannedOrder(context) { const tenants = records(await domesticGet(context, '查询PDA可用客户', '/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE')) for (const tenant of tenants) { const customerId = id(tenant?.id) if (!longId(customerId)) continue const plans = await overseas(context, 'GET', `查询客户${customerId}既有入仓计划`, query('/api/platform/v1/last-leg/inbound-plans', { customerId, page: 1, size: 200 })) if (!success(plans.response)) continue const planned = new Set(records(plans.data).map((item) => id(item?.sourceOrderId)).filter(Boolean)) const orders = await overseas(context, 'GET', `查询客户${customerId}待规划尾程单`, query('/api/platform/v1/last-leg/orders', { customerId, currentStatus: 'PENDING_ASSIGNMENT', page: 1, size: 200 })) if (!success(orders.response)) continue const order = records(orders.data).find((item) => longId(item?.id) && nonBlank(item?.containerNo) && !planned.has(id(item.id))) if (order) return { customerId, order } } return null } async function createWarehouse(context, customerId) { const suffix = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase() const warehouseCode = `PDA-${suffix}`.slice(0, 50) const created = await overseas(context, 'POST', '创建PDA入仓目标海外仓', '/api/platform/v1/warehouse-admin', { customerId, warehouseCode, warehouseName: `${context.marker} PDA入仓仓`, warehouseType: 'OVERSEAS', countryCode: 'US', timezone: 'America/Los_Angeles', contactName: 'Phase4 PDA', contactPhone: '+1-213-555-0199', contactEmail: null, status: 'ACTIVE', remark: `${context.marker}-pda-inbound`, address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California', city: 'Los Angeles', district: 'Commerce', postalCode: '90040', addressLine1: '502 Phase Four PDA Avenue', addressLine2: null, latitude: null, longitude: null }, capabilities: [{ capabilityCode: 'STORAGE', capabilityName: 'Storage', enabled: true, cutoffTime: null, description: `${context.marker} PDA inbound acceptance` }], }) const warehouseId = id(created.data) context.assert(MODULE, 'PDA目标海外仓返回长ID', created.response, longId(warehouseId), `warehouseId=${warehouseId || 'missing'}; ${trace(created.response)}`, OPTIONS) return { warehouseId, warehouseCode, warehouseName: `${context.marker} PDA入仓仓` } } async function createResource(context, warehouseId, index, fbaWarehouseCode) { const suffix = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-14).toUpperCase() const location = await overseas(context, 'POST', `创建PDA库位${index + 1}`, `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations`, { zoneCode: `PDA-${index + 1}`, locationCode: `PDA-${suffix}-${index + 1}`.slice(0, 64), locationType: 'STORAGE', maxLoadKg: 5000, status: 'IDLE', remark: context.marker, }) const locationId = id(location.data) context.assert(MODULE, `PDA库位${index + 1}返回长ID`, location.response, longId(locationId), `locationId=${locationId || 'missing'}; ${trace(location.response)}`, OPTIONS) const pallet = await overseas(context, 'POST', `创建PDA实物托盘${index + 1}`, `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, { palletCode: `PDA-${suffix}-${index + 1}`.slice(0, 128), palletType: '木托盘', capacityCbm: 2, unitPrice: 0, status: 'IDLE', locationId: longId(locationId) ? locationId : undefined, remark: context.marker, fbaWarehouseCode: fbaWarehouseCode || null, }) const palletId = id(pallet.data) context.assert(MODULE, `PDA实物托盘${index + 1}返回长ID`, pallet.response, longId(palletId), `palletId=${palletId || 'missing'}; ${trace(pallet.response)}`, OPTIONS) return { locationId, palletId } } async function operateExpectedCartons(context, customerId, planNo, task) { for (const [index, carton] of (task.expectedCartons || []).entries()) { if (carton.status === 'ON_PALLET' || carton.status === 'AVAILABLE') continue const scanned = await overseas(context, 'POST', `扫描国内冻结箱唛${index + 1}`, query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/cartons/scan`, { customerId }), { cartonMark: carton.cartonMark, planPalletId: id(carton.planPalletId), idempotencyKey: key(context, 'scan', String(index + 1)), operatorId: 'phase4-http', operatorName: '四期HTTP验收', }) context.assert(MODULE, `箱唛${index + 1}已绑定托盘`, scanned.response, scanned.data?.result === 'SCANNED' && scanned.data?.cartonMark === carton.cartonMark, `cartonMark=${carton.cartonMark || 'missing'}, result=${scanned.data?.result || 'missing'}; ${trace(scanned.response)}`, OPTIONS) } } async function operateLegacyAllocations(context, customerId, planNo, task) { for (const [index, allocation] of (task.allocations || []).entries()) { const remaining = Number(allocation.plannedPackageCount || 0) - Number(allocation.pickedPackageCount || 0) if (remaining <= 0) continue await overseas(context, 'POST', `清点入仓分配${index + 1}`, query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/allocations/${encodeURIComponent(id(allocation.allocationId))}/count`, { customerId }), { packageCount: Number(allocation.plannedPackageCount), idempotencyKey: key(context, 'count', String(index + 1)), operatorId: 'phase4-http', operatorName: '四期HTTP验收', }) await overseas(context, 'POST', `实收入仓分配${index + 1}`, query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/allocations/${encodeURIComponent(id(allocation.allocationId))}/pick`, { customerId }), { packageCount: remaining, idempotencyKey: key(context, 'pick', String(index + 1)), operatorId: 'phase4-http', operatorName: '四期HTTP验收', }) } } export async function run(context) { context.createdResources.phase4OverseasPda = { marker: context.marker, customerId: null, orderId: null, planNo: null, warehouseId: null, taskId: null, finalStatus: null, retained: true } const source = await findUnplannedOrder(context) if (!source) { context.addCoverageGap(MODULE, SCENARIO, 'PDA入仓来源尾程单', '所有ACTIVE客户均无尚未转换、状态为PENDING_ASSIGNMENT且具有柜号的尾程单;请先运行三期装柜完成事件链', 'fixture-unavailable') return } const { customerId, order } = source const warehouse = await createWarehouse(context, customerId) if (!longId(warehouse.warehouseId)) return Object.assign(context.createdResources.phase4OverseasPda, { customerId, orderId: id(order.id), warehouseId: warehouse.warehouseId }) const transferred = await overseas(context, 'PUT', '尾程单指定本轮PDA目标仓', query(`/api/platform/v1/last-leg/orders/${encodeURIComponent(id(order.id))}/destination`, { customerId }), { warehouseId: warehouse.warehouseId, warehouseCode: warehouse.warehouseCode, warehouseName: warehouse.warehouseName, remark: `${context.marker} PDA lifecycle`, }) if (!success(transferred.response)) return const converted = await overseas(context, 'POST', '尾程单转换入仓计划', '/api/platform/v1/last-leg/inbound-plans/convert', { orderId: id(order.id), customerId, }) const plan = converted.data const planNo = plan?.planNo || '' context.assert(MODULE, '入仓计划转换返回完整作业结构', converted.response, Boolean(planNo) && Array.isArray(plan?.pallets) && plan.pallets.length > 0 && Array.isArray(plan?.allocations) && plan.allocations.length > 0, `planNo=${planNo || 'missing'}, pallets=${plan?.pallets?.length ?? 'missing'}, allocations=${plan?.allocations?.length ?? 'missing'}; ${trace(converted.response)}`, OPTIONS) if (!planNo || !Array.isArray(plan?.pallets) || !plan.pallets.length) return context.createdResources.phase4OverseasPda.planNo = planNo for (const [index, planPallet] of plan.pallets.entries()) { const resource = await createResource(context, warehouse.warehouseId, index, planPallet.destinationWarehouseCode) if (!longId(resource.locationId) || !longId(resource.palletId)) return const assigned = await overseas(context, 'POST', `分配计划托盘${index + 1}`, query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/pallets/${encodeURIComponent(id(planPallet.id))}/assign`, { customerId }), { warehouseId: warehouse.warehouseId, palletId: resource.palletId, locationId: resource.locationId, idempotencyKey: key(context, 'assign', String(index + 1)), }) if (!success(assigned.response)) return } const pushed = await overseas(context, 'POST', '推送PDA入仓任务', query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/push`, { customerId }), { idempotencyKey: key(context, 'push'), operatorId: 'phase4-http', operatorName: '四期HTTP验收', }) const task = pushed.data context.assert(MODULE, 'PDA任务包含托盘、分配和箱唛指令', pushed.response, longId(task?.taskId) && Array.isArray(task?.pallets) && task.pallets.length === plan.pallets.length && Array.isArray(task?.allocations) && task.allocations.length > 0 && Array.isArray(task?.expectedCartons), `taskId=${id(task?.taskId) || 'missing'}, pallets=${task?.pallets?.length ?? 'missing'}, allocations=${task?.allocations?.length ?? 'missing'}, cartons=${task?.expectedCartons?.length ?? 'missing'}; ${trace(pushed.response)}`, OPTIONS) if (!longId(task?.taskId)) return context.createdResources.phase4OverseasPda.taskId = id(task.taskId) if ((task.expectedCartons || []).length > 0) await operateExpectedCartons(context, customerId, planNo, task) else await operateLegacyAllocations(context, customerId, planNo, task) const photo = await context.requestOverseasMultipart('POST', query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/unload-photos/files`, { customerId }), { name: `${context.marker}-unload.png`, type: 'image/png', content: Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')), }) context.addStep(MODULE, '上传真实卸柜照片文件', photo, success(photo), trace(photo), OPTIONS) const fileId = id(photo?.body?.data?.fileId) context.assert(MODULE, '卸柜照片返回长文件ID', photo, longId(fileId), `fileId=${fileId || 'missing'}; ${trace(photo)}`, OPTIONS) if (!longId(fileId)) return const bound = await overseas(context, 'POST', '绑定卸柜照片并生成预计报空时间', query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/unload-photos`, { customerId }), { fileIds: [fileId], idempotencyKey: key(context, 'photos'), operatorId: 'phase4-http', operatorName: '四期HTTP验收', }) context.assert(MODULE, '照片凭证含上传时间和预计报空时间', bound.response, bound.data?.unloadPhotoFileIds?.map(id).includes(fileId) && Boolean(bound.data?.unloadPhotoUploadedAt) && Boolean(bound.data?.estimatedEmptyReturnAt), `fileIds=${bound.data?.unloadPhotoFileIds?.map(id).join(',') || 'missing'}, estimated=${bound.data?.estimatedEmptyReturnAt || 'missing'}; ${trace(bound.response)}`, OPTIONS) const completed = await overseas(context, 'POST', '完成PDA入仓并写库存', query(`/api/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}/complete`, { customerId })) context.assert(MODULE, 'PDA任务进入COMPLETED终态', completed.response, completed.data?.status === 'COMPLETED' && Boolean(completed.data?.completedAt), `status=${completed.data?.status || 'missing'}, completedAt=${completed.data?.completedAt || 'missing'}; ${trace(completed.response)}`, OPTIONS) context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null const planAfter = await overseas(context, 'GET', '回查入仓计划完成状态', query(`/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, { customerId })) context.assert(MODULE, '入仓计划与PDA任务同步完成', planAfter.response, planAfter.data?.status === 'COMPLETED', `planStatus=${planAfter.data?.status || 'missing'}; ${trace(planAfter.response)}`, OPTIONS) const balances = await overseas(context, 'GET', '回查PDA入仓SKU库存', query('/api/platform/v1/last-leg/inventory/balances', { customerId, page: 1, size: 200 })) context.assert(MODULE, 'PDA完成后生成SKU库存余额', balances.response, records(balances.data).some((item) => Number(item?.totalPackages) > 0 && id(item?.customerId) === customerId), `balances=${records(balances.data).length}; ${trace(balances.response)}`, OPTIONS) }