import { readFile, writeFile } from 'node:fs/promises' import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' const domestic = process.env.ADMIN_TEST_GATEWAY || 'http://127.0.0.1:9000' const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300' const sourceReport = process.env.OVERSEAS_ACCEPTANCE_SOURCE const directDispatchNo = process.env.OVERSEAS_DISPATCH_NO const directRelabelNo = process.env.OVERSEAS_RELABEL_NO const output = process.env.OVERSEAS_FULFILLMENT_OUTPUT || 'reports/overseas-relabel-fulfillment-acceptance.json' const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID const directPlatformToken = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN const marker = `FUL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}` if (!directPlatformToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing platform credentials or token') if (!directMerchantToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing merchant token or platform credentials') if (!directPdaToken && (!process.env.PDA_TEST_USERNAME || !process.env.PDA_TEST_PASSWORD)) throw new Error('Missing PDA credentials or token') if ((!sourceReport && !directDispatchNo) || !customerId) throw new Error('Missing acceptance source/dispatch or customer id') function id(value) { return value == null ? '' : String(value) } function key(value) { return `${marker}-${value}`.slice(0, 200) } function assert(value, message) { if (!value) throw new Error(message) } async function api(base, token, method, path, body) { const response = await fetch(`${base}${path}`, { method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json' }) }, body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body) }) const text = await response.text(); let value try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } } if (!response.ok || String(value?.code) !== '200') { throw new Error(`${method} ${path}: HTTP=${response.status}, code=${value?.code}, message=${value?.message}`) } return value.data } const source = sourceReport ? JSON.parse(await readFile(sourceReport, 'utf8')) : null const scenario = source?.scenarios.find((value) => value.name === 'SPLIT-TWO-TARGETS') || { dispatchNo: directDispatchNo, relabelNo: directRelabelNo || '' } assert(scenario?.dispatchNo, 'Dispatch not found in source report') const dispatchNo = scenario.dispatchNo const platformLogin = directPlatformToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: process.env.ADMIN_TEST_USERNAME, password: process.env.ADMIN_TEST_PASSWORD }) const pdaLogin = directPdaToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: process.env.PDA_TEST_USERNAME, password: process.env.PDA_TEST_PASSWORD }) const ticket = directMerchantToken ? null : await api(domestic, platformLogin.accessToken, 'POST', `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} 海外尾程履约验收` }) const merchantLogin = directMerchantToken ? null : await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code }) const platformToken = directPlatformToken || platformLogin.overseasAccessToken const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken const pdaToken = directPdaToken || pdaLogin.overseasAccessToken let task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) let alreadyDeparted = task.status === 'DEPARTED' assert(alreadyDeparted || task.tripNo, 'Prepare and bind a qualified fleet trip before running retained dispatch fulfillment') const existingPage = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/delivery-orders?customerId=${customerId}&page=1&size=20&dispatchNo=${encodeURIComponent(dispatchNo)}`) const existingDeliveries = (existingPage.records || []).filter((value) => value.dispatchNo === dispatchNo) const existingOutboundNos = new Set(existingDeliveries.map((value) => value.outboundNo).filter(Boolean)) assert(existingOutboundNos.size <= 1, 'Dispatch is linked to multiple outbound orders') const transfer = existingDeliveries.length > 0 ? { outboundNo: [...existingOutboundNos][0], deliveries: existingDeliveries.map((value) => ({ deliveryNo: value.deliveryNo, deliveryStatus: value.status, destinationWarehouseId: value.destinationWarehouseId, destinationWarehouseCode: value.destinationWarehouseCode, destinationWarehouseName: value.destinationWarehouseName, palletCount: value.palletCount, packageCount: value.packageCount, })), replayed: true, } : await api(overseas, merchantToken, 'POST', `/api/merchant/v1/last-leg/delivery-orders?dispatchNo=${encodeURIComponent(dispatchNo)}`, { idempotencyKey: key('TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172800000).toISOString(), deliveryMethod: 'TRUCK_PALLET', waybillNo: `${marker}-WAYBILL`, carrierName: '尾程验收车队', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '15托', appointmentAccount: 'SELF', remark: marker, fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 10, remark: marker }], }) const transferDeliveries = Array.isArray(transfer.deliveries) ? transfer.deliveries : [] assert(transfer.outboundNo && transferDeliveries.length > 0, 'Transfer did not create outbound and delivery') assert(transferDeliveries.length === 1, `Expected one destination delivery, got ${transferDeliveries.length}`) const deliveryNo = transferDeliveries[0].deliveryNo const expectedPdaItems = scenario.partialRelabel ? 1 : 2 assert(Array.isArray(task.items) && task.items.length === expectedPdaItems, `Expected ${expectedPdaItems} PDA pallet items`) if (!alreadyDeparted) { for (const [itemIndex, item] of task.items.entries()) { let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages) for (const [positionIndex, position] of item.positions.entries()) { if (remaining <= 0) break const packages = Math.min(remaining, Number(position.availablePackages)) if (packages <= 0) continue await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/dispatches/${dispatchNo}/items/${id(item.dispatchItemId)}/pick`, { positionId: id(position.id), packages, productQuantity: packages, idempotencyKey: key(`PICK-${itemIndex}-${positionIndex}`), remark: marker, }) remaining -= packages } assert(remaining === 0, `PDA item ${item.dispatchItemId} has insufficient inventory`) } task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) alreadyDeparted = task.status === 'DEPARTED' } if (!alreadyDeparted && task.status !== 'COMPLETED') { const pickingCompleted = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/dispatches/${dispatchNo}/complete`, { idempotencyKey: key('PICK-COMPLETE') }) assert(pickingCompleted.status === 'READY_TO_DEPART', 'Dispatch did not reach READY_TO_DEPART') task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) } assert(alreadyDeparted || task.status === 'COMPLETED', 'Dispatch did not finish picking') if (!alreadyDeparted) { const tripPath = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(task.tripNo)}` const trip = await api(overseas, platformToken, 'GET', tripPath) const loading = await api(overseas, platformToken, 'GET', `${tripPath}/loading`) assert(!loading.blockReason, `Trip not ready: ${loading.blockReason}`) const confirmed = await api(overseas, platformToken, 'POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion, manifestHash: loading.manifestHash, pallets: controlledPalletMeasurements(loading.cargo, trip.trip.stops), reason: '受控测试测量,每实托100kg/1m³/1托位', }) const departed = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/dispatches/${dispatchNo}/depart`, { recordVersion: confirmed.recordVersion, idempotencyKey: key('DEPART').slice(0,128), remark: marker }) assert(departed.status === 'IN_TRANSIT' && departed.dispatches?.some(item => item.dispatchNo === dispatchNo), 'Whole-trip departure missing dispatch') } const departedDispatch = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`) assert(departedDispatch.status === 'DEPARTED', 'Dispatch did not reach DEPARTED') const outbound = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}?page=1&size=20`) assert(outbound.outbound.status === 'OUTBOUNDED', 'Outbound did not reach OUTBOUNDED') let delivery = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) if (delivery.order.status === 'WAIT_DELIVERY') { const delivering = await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/status?customerId=${customerId}`, { status: 'DELIVERING', expectedVersion: Number(delivery.order.version), remark: marker, }) assert(delivering.deliveryStatus === 'DELIVERING', 'Delivery did not reach DELIVERING') delivery = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) } if (delivery.order.status !== 'DELIVERED') { assert(delivery.order.status === 'DELIVERING', `Unexpected delivery status ${delivery.order.status}`) const documentsPath = `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents?customerId=${customerId}` const documents = await api(overseas, platformToken, 'GET', documentsPath) const form = new FormData() form.append('file', new Blob([`${marker} receipt acceptance for ${deliveryNo}`], { type: 'text/plain' }), `${marker}-${deliveryNo}-receipt.txt`) const receipt = await api(overseas, platformToken, 'POST', `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents/RECEIPT/files?customerId=${customerId}`, form) const existingBindings = (documents.categories || []).map((category) => ({ category: category.category, fileIds: (category.files || []).map((file) => id(file.fileId)), })).filter((binding) => binding.category !== 'RECEIPT' && binding.fileIds.length > 0) await api(overseas, platformToken, 'PUT', documentsPath, { expectedVersion: Number(documents.version), idempotencyKey: key('SAVE-RECEIPT'), bolNo: documents.bolNo || null, documents: [...existingBindings, { category: 'RECEIPT', fileIds: [id(receipt.fileId)] }], }) delivery = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) } assert(delivery.order.status === 'DELIVERED', 'Receipt did not automatically complete delivery') const visits = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/customer-visits?page=1&size=200&customerId=${customerId}&keyword=${encodeURIComponent(deliveryNo)}`) const visit = (visits.records || []).find((value) => value.deliveryNo === deliveryNo) assert(visit?.status === 'PENDING', 'Delivery completion did not create a pending customer visit') const report = { marker, scope: 'OVERSEAS_LASTLEG_ONLY', sourceReport, relabelNo: scenario.relabelNo, dispatchNo, dispatchStatus: departedDispatch.status, outboundNo: transfer.outboundNo, outboundStatus: outbound.outbound.status, deliveryNo, deliveryStatus: delivery.order.status, visitId: id(visit.id), shippedPackages: departedDispatch.shippedPackageCount, passed: true, finishedAt: new Date().toISOString() } await writeFile(output, JSON.stringify(report, null, 2), 'utf8') console.log(JSON.stringify(report, null, 2))