const MODULE = 'phase4-overseas-inbound-operations' const OPTIONS = { scenario: 'phase4-inbound-admin-and-inventory-commands', deploymentBlocker: true } const PLAN_BASE = '/api/platform/v1/last-leg/inbound-plans' const INVENTORY_BASE = '/api/platform/v1/last-leg/inventory' function id(value) { return value == null ? '' : String(value) } function ok(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function rows(data) { return Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [] } function conflict(response) { return response?.http === 200 && ['A0409', '409'].includes(String(response?.body?.code)) } function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function query(path, values) { const params = new URLSearchParams() for (const [name, value] of Object.entries(values || {})) if (value != null && value !== '') params.set(name, String(value)) return `${path}?${params.toString()}` } function key(context, operation) { return `${context.marker}:P4-INBOUND:${operation}`.slice(0, 255) } async function request(context, method, name, path, body, headers = {}) { const response = await context.request(method, path, body, undefined, headers) context.addStep(MODULE, name, response, ok(response), trace(response), OPTIONS) return ok(response) ? response.body?.data : null } async function inboundAdminLifecycle(context) { const matrix = context.createdResources?.phase4InboundProducerFixtures const completed = new Set((context.createdResources?.phase4OverseasPda?.completedContainers || []).map(id)) const producer = (matrix?.byDestinationType?.FBA || []).find((item) => !completed.has(id(item?.containerNo))) context.assert(MODULE, '选择本轮未被PDA完成的FBA第二柜作为Admin写链夹具', null, Boolean(producer?.containerNo && producer?.tenantId), `containerNo=${producer?.containerNo || 'missing'}, customerId=${producer?.tenantId || 'missing'}`, OPTIONS) if (!producer?.containerNo || !producer?.tenantId) return const customerId = id(producer.tenantId) const orderPage = await request(context, 'GET', '按第二柜查询尾程订单', query('/api/platform/v1/last-leg/orders', { page: 1, size: 200, customerId, containerNo: producer.containerNo, })) const order = rows(orderPage).find((item) => id(item?.customerId) === customerId && id(item?.containerNo) === id(producer.containerNo) && item?.destinationType === 'FBA') context.assert(MODULE, '第二柜尾程订单身份稳定', null, Boolean(order?.id), `orderId=${id(order?.id) || 'missing'}, containerNo=${producer.containerNo}`, OPTIONS) if (!order?.id) return const bookingId = id(producer.containerBookingId) let preview = await request(context, 'GET', '预览第二柜待生成入仓计划', query(`${PLAN_BASE}/container-preview`, { sourceContainerBookingId: bookingId, })) const arrivalQuery = await context.request('POST', '/api/platform/v1/last-leg/orders/container-arrival-times/query', { sourceContainerBookingIds: [bookingId] }) const arrival = ok(arrivalQuery) && Array.isArray(arrivalQuery.body?.data) ? arrivalQuery.body.data.find((item) => id(item?.sourceContainerBookingId) === bookingId) : null if (arrival && !arrival.estimatedWarehouseArrivalAt) { await context.request('PUT', `/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`, { estimatedWarehouseArrivalAt: new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 19), expectedVersion: Number(arrival.version), remark: `${context.marker}-inbound-plan-prerequisite` }, undefined, { 'Idempotency-Key': `${context.marker}-inbound-plan-arrival-${bookingId}`.slice(0, 128) }) preview = await request(context, 'GET', '刷新补齐时间后的入仓计划预览', query(`${PLAN_BASE}/container-preview`, { sourceContainerBookingId: bookingId, })) } const convertBody = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview?.snapshotVersion), snapshotHash: preview?.snapshotHash } const convertHeaders = { 'Idempotency-Key': key(context, 'convert-container') } const plans = await request(context, 'POST', '尾程入仓单转换为入仓计划', `${PLAN_BASE}/convert-container`, convertBody, convertHeaders) const replayedPlans = await request(context, 'POST', '重复转换返回同一入仓计划', `${PLAN_BASE}/convert-container`, convertBody, convertHeaders) const plan = (plans || []).find((item) => (item?.lines || []).some((line) => id(line?.orderId) === id(order.id))) || plans?.[0] const replay = (replayedPlans || []).find((item) => item?.planNo === plan?.planNo) || replayedPlans?.[0] context.assert(MODULE, '入仓计划转换天然幂等', null, Boolean(plan?.planNo) && replay?.planNo === plan.planNo, `first=${plan?.planNo || 'missing'}, replay=${replay?.planNo || 'missing'}`, OPTIONS) const planNo = plan?.planNo const fbaLines = (plan?.lines || []).filter((line) => line?.fbaWarehouseCode && line?.destinationWarehouseId) context.assert(MODULE, 'FBA入仓计划包含可预约明细', null, Boolean(planNo) && fbaLines.length > 0, `planNo=${planNo || 'missing'}, fbaLines=${fbaLines.length}`, OPTIONS) if (!planNo || fbaLines.length === 0) return const sample = fbaLines[0] const supplement = await request(context, 'POST', '为未完成FBA计划补充可打印托盘', `${PLAN_BASE}/${encodeURIComponent(planNo)}/pallets/supplement`, { destinationWarehouseId: id(sample.destinationWarehouseId), destinationWarehouseCode: sample.destinationWarehouseCode, destinationWarehouseName: sample.destinationWarehouseName, customerId, }) context.assert(MODULE, '补充托盘绑定计划内目的仓且保持未分配', null, Boolean(supplement?.id) && supplement?.assignmentStatus === 'UNASSIGNED' && id(supplement?.destinationWarehouseId) === id(sample.destinationWarehouseId), `palletId=${id(supplement?.id) || 'missing'}, status=${supplement?.assignmentStatus || 'missing'}`, OPTIONS) const suffix = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase() const now = Date.now() const appointmentBody = { originWarehouseId: id(plan.overseasWarehouseId), fbaWarehouseCode: sample.fbaWarehouseCode, lineIds: fbaLines.map((line) => id(line.id)), idempotencyKey: `fba-${suffix}`, details: { plannedDate: new Date(now + 24 * 60 * 60_000).toISOString().slice(0, 10), appointmentStatus: 'PENDING', appointmentRemark: context.marker }, } const appointment = await request(context, 'POST', '批量创建FBA预约', `${PLAN_BASE}/fba-appointments/batch`, appointmentBody) const appointmentNo = appointment.appointmentNo const expectedSources = new Set(fbaLines.map(line => id(line.orderId))).size const appointmentReplay = await request(context, 'POST', 'FBA预约同单号同载荷重放', `${PLAN_BASE}/fba-appointments/batch`, appointmentBody) context.assert(MODULE, 'FBA预约重放返回同一聚合且明细一致', null, id(appointmentReplay?.id) === id(appointment?.id) && appointmentReplay?.items?.length === expectedSources, `appointmentId=${id(appointment?.id) || 'missing'}, items=${appointmentReplay?.items?.length ?? 'missing'}`, OPTIONS) const appointmentConflict = await context.request('POST', `${PLAN_BASE}/fba-appointments/batch`, { ...appointmentBody, details: { ...appointmentBody.details, appointmentRemark: `${context.marker}-different` }, }) context.addStep(MODULE, 'FBA预约同单号异载荷返回A0409', appointmentConflict, conflict(appointmentConflict), trace(appointmentConflict), OPTIONS) const detail = await request(context, 'GET', '查询FBA预约详情', `${PLAN_BASE}/fba-appointments/${encodeURIComponent(appointmentNo)}`) context.assert(MODULE, 'FBA预约详情冻结仓码和所选明细', null, detail?.appointmentNo === appointmentNo && detail?.fbaWarehouseCode === sample.fbaWarehouseCode && detail?.items?.length === expectedSources, `appointmentNo=${detail?.appointmentNo || 'missing'}, warehouse=${detail?.fbaWarehouseCode || 'missing'}`, OPTIONS) const cancelled = await request(context, 'PUT', '取消本轮FBA预约收口', `${PLAN_BASE}/fba-appointments/${encodeURIComponent(appointmentNo)}`, { idempotencyKey: `fba-cancel-${suffix}`, version: detail.version, details: { appointmentStatus: 'CANCELLED', appointmentRemark: `${context.marker}-cancelled` }, }) context.assert(MODULE, 'FBA预约最终为CANCELLED', null, cancelled?.appointmentStatus === 'CANCELLED', `status=${cancelled?.appointmentStatus || 'missing'}`, OPTIONS) context.createdResources.phase4OverseasInboundAdmin = { marker: context.marker, customerId, orderId: id(order.id), planNo, supplementedPalletId: id(supplement?.id) || null, appointmentNo, terminalStatus: cancelled?.appointmentStatus || null, } } async function inventoryCommandLifecycle(context) { const pda = context.createdResources?.phase4OverseasPda const customerId = id(pda?.customerId) const palletIds = new Set((pda?.palletIds || []).map(id)) context.assert(MODULE, '库存命令只使用本轮PDA完成柜形成的库存', null, Boolean(customerId && pda?.finalStatus === 'COMPLETED' && palletIds.size > 0), `customerId=${customerId || 'missing'}, pallets=${palletIds.size}, status=${pda?.finalStatus || 'missing'}`, OPTIONS) if (!customerId || pda?.finalStatus !== 'COMPLETED' || palletIds.size === 0) return const balancePage = await request(context, 'GET', '查询本轮客户库存余额', query(`${INVENTORY_BASE}/balances`, { customerId, overseasWarehouseId: pda.overseasWarehouseId, page: 1, size: 200, })) let selected = null for (const balance of rows(balancePage)) { const positions = await request(context, 'GET', `查询库存余额${id(balance.id)}库位`, query( `${INVENTORY_BASE}/balances/${encodeURIComponent(id(balance.id))}/positions`, { customerId })) const position = rows(positions).find((item) => palletIds.has(id(item?.palletId)) && Number(item?.availablePackages) >= 1) if (position) { selected = { balance, position }; break } } context.assert(MODULE, '定位本轮PDA托盘上的可用库存位置', null, Boolean(selected?.position?.id), `positionId=${id(selected?.position?.id) || 'missing'}, palletId=${id(selected?.position?.palletId) || 'missing'}`, OPTIONS) if (!selected?.position?.id) return const positionId = id(selected.position.id) const baseline = { available: Number(selected.position.availablePackages), hold: Number(selected.position.holdPackages), allocated: Number(selected.position.allocatedPackages), picked: Number(selected.position.pickedPackages), } const businessOrderNo = `${context.marker}-INVENTORY` const holdBody = { positionId, packages: 1, businessOrderNo, idempotencyKey: key(context, 'HOLD'), remark: context.marker } const held = await request(context, 'POST', '冻结一箱本轮PDA库存', `${INVENTORY_BASE}/hold`, holdBody) const heldReplay = await request(context, 'POST', '库存冻结同键重放', `${INVENTORY_BASE}/hold`, holdBody) context.assert(MODULE, '库存冻结重放不重复扣减', null, held?.operationType === 'HOLD' && heldReplay?.operationId === held?.operationId && heldReplay?.replayed === true, `operationId=${id(held?.operationId) || 'missing'}, replayed=${heldReplay?.replayed}`, OPTIONS) const holdConflict = await context.request('POST', `${INVENTORY_BASE}/hold`, { ...holdBody, remark: `${context.marker}-different`, }) context.addStep(MODULE, '库存冻结同键异载荷返回A0409', holdConflict, conflict(holdConflict), trace(holdConflict), OPTIONS) const releaseBody = { positionId, packages: 1, businessOrderNo, idempotencyKey: key(context, 'RELEASE'), remark: context.marker } const released = await request(context, 'POST', '释放一箱冻结库存', `${INVENTORY_BASE}/release`, releaseBody) const releasedReplay = await request(context, 'POST', '库存释放同键重放', `${INVENTORY_BASE}/release`, releaseBody) context.assert(MODULE, '库存释放重放不重复增加可用数', null, released?.operationType === 'RELEASE' && releasedReplay?.operationId === released?.operationId && releasedReplay?.replayed === true, `operationId=${id(released?.operationId) || 'missing'}, replayed=${releasedReplay?.replayed}`, OPTIONS) const increaseBody = { positionId, packageDelta: 1, reason: 'ACCEPTANCE_COUNT_GAIN', businessOrderNo, idempotencyKey: key(context, 'ADJUST-UP'), remark: context.marker } const increased = await request(context, 'POST', '盘点调增一箱', `${INVENTORY_BASE}/adjust`, increaseBody) const increaseReplay = await request(context, 'POST', '盘点调增同键重放', `${INVENTORY_BASE}/adjust`, increaseBody) context.assert(MODULE, '盘点调增重放不重复入账', null, increased?.operationType === 'ADJUST' && increaseReplay?.operationId === increased?.operationId && increaseReplay?.replayed === true, `operationId=${id(increased?.operationId) || 'missing'}, replayed=${increaseReplay?.replayed}`, OPTIONS) const adjustConflict = await context.request('POST', `${INVENTORY_BASE}/adjust`, { ...increaseBody, reason: 'DIFFERENT_COUNT_REASON', }) context.addStep(MODULE, '盘点调整同键异载荷返回A0409', adjustConflict, conflict(adjustConflict), trace(adjustConflict), OPTIONS) await request(context, 'POST', '盘点调减一箱恢复基线', `${INVENTORY_BASE}/adjust`, { positionId, packageDelta: -1, reason: 'ACCEPTANCE_COUNT_RESTORE', businessOrderNo, idempotencyKey: key(context, 'ADJUST-DOWN'), remark: context.marker, }) const finalPositions = await request(context, 'GET', '回查库存命令后的托盘位置', query( `${INVENTORY_BASE}/balances/${encodeURIComponent(id(selected.balance.id))}/positions`, { customerId })) const finalPosition = rows(finalPositions).find((item) => id(item?.id) === positionId) context.assert(MODULE, '冻结释放与盘点正反向后库存数量恢复基线', null, Number(finalPosition?.availablePackages) === baseline.available && Number(finalPosition?.holdPackages) === baseline.hold && Number(finalPosition?.allocatedPackages) === baseline.allocated && Number(finalPosition?.pickedPackages) === baseline.picked, `before=${JSON.stringify(baseline)}, after=${JSON.stringify({ available: finalPosition?.availablePackages, hold: finalPosition?.holdPackages, allocated: finalPosition?.allocatedPackages, picked: finalPosition?.pickedPackages })}`, OPTIONS) const ledger = await request(context, 'GET', '按本轮业务号回查库存流水', query(`${INVENTORY_BASE}/ledger`, { customerId, businessOrderNo, page: 1, size: 100, })) const types = rows(ledger).map((item) => item?.businessType) context.assert(MODULE, '库存流水包含HOLD、RELEASE及两条ADJUST且重放零重复', null, types.filter((type) => type === 'HOLD').length === 1 && types.filter((type) => type === 'RELEASE').length === 1 && types.filter((type) => type === 'ADJUST').length === 2, `types=${types.join(',') || 'missing'}`, OPTIONS) context.createdResources.phase4OverseasInventoryCommands = { marker: context.marker, customerId, balanceId: id(selected.balance.id), positionId, terminalState: 'RESTORED', } } export async function run(context) { context.createdResources.phase4OverseasInboundAdmin = { marker: context.marker } context.createdResources.phase4OverseasInventoryCommands = { marker: context.marker } await inboundAdminLifecycle(context) await inventoryCommandLifecycle(context) }