From c71db4b48147add456599d7ec65cacde97b25866 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 17 Aug 2026 16:45:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/OrderFulfillmentSplitService.java | 10 + .../src/main/resources/openapi/order-v1.yaml | 2 +- .../OrderFulfillmentSplitServiceTest.java | 7 + ...te-pending-review-orders-contract.test.mjs | 23 +- scripts/create-pending-review-orders.mjs | 60 +++-- scripts/pda-final-complete-diagnostic.mjs | 223 ++++++++++++++++++ 6 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 scripts/pda-final-complete-diagnostic.mjs diff --git a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java index a46cf63b6..524868b10 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java @@ -141,11 +141,21 @@ public class OrderFulfillmentSplitService { child.setDestinationWarehouseName(root.getDestinationWarehouseName()); child.setReceivingWarehouseId(root.getReceivingWarehouseId()); child.setReceivingWarehouseName(root.getReceivingWarehouseName()); child.setChannelCode(root.getChannelCode()); child.setChannelName(root.getChannelName()); child.setDestinationType(root.getDestinationType()); + child.setInboundCode(remainderInboundCode(root, sequence)); child.setCreatedBy(operatorId); child.setUpdatedBy(operatorId); if (orders.insert(child) != 1) throw new BizException(ResultCode.CONFLICT, "余量子订单创建失败"); return child; } + private String remainderInboundCode(OrderEntity root, int sequence) { + String rootInboundCode = Optional.ofNullable(root.getInboundCode()).orElse("").trim(); + String childInboundCode = rootInboundCode + "-" + sequence; + if (rootInboundCode.isEmpty() || sequence < 1 || childInboundCode.length() > 40) { + throw new BizException(ResultCode.CONFLICT, "根订单入仓码格式无效,无法创建余量子订单"); + } + return childInboundCode; + } + private OrderFulfillmentSplitEntity ledger(OrderSplitDtos.CreateRequest r, OrderEntity root, OrderEntity source, long childId, String role, String hash, long operatorId) { OrderFulfillmentSplitEntity row = new OrderFulfillmentSplitEntity(); diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index 65ea14255..08b282327 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -621,7 +621,7 @@ components: success: {type: boolean} orderId: {type: string, pattern: '^[1-9][0-9]*$'} orderNo: {type: string} - inboundCode: {type: string, pattern: '^[A-Z]{4,6}[0-9]{10,}$', example: ZBYHK2608140001} + inboundCode: {type: string, pattern: '^[A-Z]{4,6}[0-9]{10,}(?:-[1-9][0-9]*)?$', example: ZBYHK2608140001, description: 根订单使用渠道前缀+业务日期+全局票序;履约拆分产生的余量子订单追加根范围 splitSequence 后缀,如 ZBYHK2608140001-1} errorCode: {type: string} errorMessage: {type: string} FirstLegOrderCargo: diff --git a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java index 1597c37c4..7636a6b38 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java @@ -53,6 +53,11 @@ class OrderFulfillmentSplitServiceTest { assertThat(response.loadedChild().sourceOrderId()).isEqualTo(100L); assertThat(response.loadedChild().loadingPlanId()).isEqualTo(701L); assertThat(response.remainderChild().containerBookingId()).isEqualTo(801L); + ArgumentCaptor childOrder = ArgumentCaptor.forClass(OrderEntity.class); + verify(orders).insert(childOrder.capture()); + assertThat(childOrder.getValue().getInboundCode()) + .isEqualTo("ZBYHK2608170001-1") + .isNotEqualTo(parent.getInboundCode()); ArgumentCaptor rows = ArgumentCaptor.forClass(OrderFulfillmentSplitEntity.class); verify(splits, times(2)).insert(rows.capture()); assertThat(rows.getAllValues()).allSatisfy(row -> { @@ -174,6 +179,7 @@ class OrderFulfillmentSplitServiceTest { OrderEntity child = invocation.getArgument(0); assertThat(child.getOrderNo()).isEqualTo("ORDER-001-2"); assertThat(child.getParentOrderId()).isEqualTo(100L); + assertThat(child.getInboundCode()).isEqualTo("ZBYHK2608170001-2"); return 1; }); when(splits.insert(any(OrderFulfillmentSplitEntity.class))).thenReturn(1); @@ -209,6 +215,7 @@ class OrderFulfillmentSplitServiceTest { private static OrderEntity parent() { OrderEntity parent = new OrderEntity(); parent.setId(100L); parent.setTenantId(10L); parent.setOrderNo("ORDER-001"); + parent.setInboundCode("ZBYHK2608170001"); parent.setOrderRole("MASTER"); parent.setVersion(3); parent.setStatus(OrderStatus.PROCESSING); return parent; } diff --git a/scripts/create-pending-review-orders-contract.test.mjs b/scripts/create-pending-review-orders-contract.test.mjs index e30ca92c6..14b95da78 100644 --- a/scripts/create-pending-review-orders-contract.test.mjs +++ b/scripts/create-pending-review-orders-contract.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' -import { PRODUCT_CATALOG, buildOrderPayload, diversifyRoutes, parseArguments, validateCount } from './create-pending-review-orders.mjs' +import { PRODUCT_CATALOG, buildOrderPayload, diversifyRoutes, parseArguments, randomOrderOptions, validateCount } from './create-pending-review-orders.mjs' const source = readFileSync(new URL('./create-pending-review-orders.mjs', import.meta.url), 'utf8') @@ -50,6 +50,11 @@ test('builds an order payload owned by the fixed profile', () => { }, product: PRODUCT_CATALOG[0], packageCount: 7, + options: { + tradeTerm: 'EXW', pickupMethod: 'SELF_TRUCK', taxMode: 'TAX_REFUND', + customsInspection: true, expectedShipDays: 15, + senderLocation: { province: '浙江省', city: '义乌市', district: '稠城街道', postalCode: '322000' }, + }, productImageFileId: '33', }) assert.equal(payload.receivingWarehouseId, '11') @@ -58,6 +63,11 @@ test('builds an order payload owned by the fixed profile', () => { assert.equal(payload.customerName, '固定测试客户') assert.equal(payload.cargoes.length, 1) assert.equal(payload.cargoes[0].productImageFileId, '33') + assert.equal(payload.tradeTerm, 'EXW') + assert.equal(payload.pickupMethod, 'SELF_TRUCK') + assert.equal(payload.taxMode, 'TAX_REFUND') + assert.equal(payload.customsInspection, true) + assert.equal(payload.sender, null) assert.equal(payload.cargoes[0].cargoName, PRODUCT_CATALOG[0].cargoName) assert.equal(payload.cargoes[0].packageCount, 7) assert.equal(payload.cargoes[0].totalQuantity, PRODUCT_CATALOG[0].unitsPerPackage * 7) @@ -65,6 +75,17 @@ test('builds an order payload owned by the fixed profile', () => { assert.equal(payload.cargoes[0].declaredValue, PRODUCT_CATALOG[0].unitDeclaredValue * PRODUCT_CATALOG[0].unitsPerPackage * 7) }) +test('randomizes only supported basic-order fields', () => { + for (let index = 0; index < 100; index += 1) { + const options = randomOrderOptions() + assert.ok(['FOB', 'EXW', 'CIF', 'DDP'].includes(options.tradeTerm)) + assert.ok(['SELF_EXPRESS', 'PICKUP', 'SELF_TRUCK'].includes(options.pickupMethod)) + assert.ok(['BUY_DECLARATION', 'TAX_REFUND'].includes(options.taxMode)) + assert.equal(typeof options.customsInspection, 'boolean') + assert.ok(options.expectedShipDays >= 7 && options.expectedShipDays <= 60) + } +}) + test('interleaves different logistics combinations before repeating one', () => { const route = (receivingId, channelCode, pricingTaxMode, destinationCode) => ({ receiving: { id: receivingId }, channel: { channelCode }, pricingTaxMode, diff --git a/scripts/create-pending-review-orders.mjs b/scripts/create-pending-review-orders.mjs index 42d9ced20..e194efa4b 100644 --- a/scripts/create-pending-review-orders.mjs +++ b/scripts/create-pending-review-orders.mjs @@ -8,6 +8,15 @@ import { pathToFileURL } from 'node:url' const MAX_COUNT = 100 const MAX_ROUTE_POOL_SIZE = 40 const MAX_DESTINATIONS_PER_LOGISTICS = 3 +const TRADE_TERMS = ['FOB', 'EXW', 'CIF', 'DDP'] +const PICKUP_METHODS = ['SELF_EXPRESS', 'PICKUP', 'SELF_TRUCK'] +const TAX_MODES = ['BUY_DECLARATION', 'TAX_REFUND'] +const SENDER_LOCATIONS = [ + { province: '广东省', city: '深圳市', district: '南山区', postalCode: '518000' }, + { province: '广东省', city: '东莞市', district: '南城街道', postalCode: '523000' }, + { province: '浙江省', city: '义乌市', district: '稠城街道', postalCode: '322000' }, + { province: '福建省', city: '厦门市', district: '湖里区', postalCode: '361000' }, +] export const PRODUCT_CATALOG = [ { cargoName: '蓝牙音箱', cargoNameEn: 'Bluetooth speaker', hsCode: '8518210000', material: '塑料和电子元件 / Plastic and electronic components', usage: '音频播放 / Audio playback', brand: 'SOUNDTEST', modelPrefix: 'SPK', lengthCm: 42, widthCm: 32, heightCm: 28, unitsPerPackage: 8, cartonGrossWeightKg: 12.8, unitDeclaredValue: 18.5, productLengthCm: 18, productWidthCm: 10, productHeightCm: 8 }, @@ -85,6 +94,10 @@ function shuffled(values) { return result } +function pickRandom(values) { + return values[randomInt(values.length)] +} + function round(value, scale) { return Number(value.toFixed(scale)) } @@ -110,6 +123,17 @@ export function diversifyRoutes(routes) { return result } +export function randomOrderOptions() { + return { + tradeTerm: pickRandom(TRADE_TERMS), + pickupMethod: pickRandom(PICKUP_METHODS), + taxMode: pickRandom(TAX_MODES), + customsInspection: randomInt(2) === 1, + expectedShipDays: randomInt(7, 61), + senderLocation: pickRandom(SENDER_LOCATIONS), + } +} + function requireSuccess(response, action) { if (response.http !== 200 || String(response.body?.code) !== '200') { throw new Error(`${action} failed: http=${response.http}, code=${response.body?.code ?? 'missing'}, message=${response.body?.message ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'}`) @@ -214,7 +238,7 @@ async function uploadProductImage(gateway, timeoutMs, token, marker) { return data.fileId } -export function buildOrderPayload({ marker, index, profile, master, product, packageCount, productImageFileId }) { +export function buildOrderPayload({ marker, index, profile, master, product, packageCount, options = randomOrderOptions(), productImageFileId }) { const sequence = String(index).padStart(3, '0') const shipmentId = `SHIP-${sequence}-${marker}`.slice(0, 64) const referenceId = `REF-${sequence}-${marker}`.slice(0, 64) @@ -228,39 +252,39 @@ export function buildOrderPayload({ marker, index, profile, master, product, pac customerId: profile.id, customerName: companyName, transportMode: 'SEA', - tradeTerm: 'FOB', + tradeTerm: options.tradeTerm, destinationCountryCode: master.destination.countryCode || 'US', destinationWarehouseId: master.destination.id, destinationWarehouseName: master.destination.warehouseName, - expectedShipDate: addDays(new Date(), 30), - customerRemark: `待审核订单 ${sequence} ${marker}`, - pickupMethod: 'PICKUP', + expectedShipDate: addDays(new Date(), options.expectedShipDays), + customerRemark: `基础待审核订单 ${sequence} ${marker} ${options.tradeTerm}`, + pickupMethod: options.pickupMethod, receivingWarehouseId: master.receiving.id, receivingWarehouseName: master.receiving.warehouseName, channelCode: master.channel.channelCode, channelName: master.channel.channelName, carrierId: null, carrierName: null, - taxMode: 'BUY_DECLARATION', + taxMode: options.taxMode, pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL', - customsInspection: false, + customsInspection: options.customsInspection, destinationType: 'FBA', amazonWarehouseCode: master.destination.warehouseCode, shipmentId, referenceId, - sender: { + sender: options.pickupMethod === 'PICKUP' ? { companyName, contactName: '订单测试联系人', phone: '13800138000', email: 'pending-order@example.com', countryCode: 'CN', - province: '广东省', - city: '深圳市', - district: '南山区', + province: options.senderLocation.province, + city: options.senderLocation.city, + district: options.senderLocation.district, addressLine: `${marker} 固定客户测试地址`, - postalCode: '518000', - }, + postalCode: options.senderLocation.postalCode, + } : null, receiver: null, cargoes: [{ skuNo: `SKU-${sequence}-${marker}`.slice(0, 64), @@ -356,7 +380,8 @@ export async function main(argv = process.argv.slice(2)) { const master = randomizedRoutes[(index - 1) % randomizedRoutes.length] const product = randomizedProducts[(index - 1) % randomizedProducts.length] const packageCount = randomInt(1, 31) - const payload = buildOrderPayload({ marker, index, profile, master, product, packageCount, productImageFileId }) + const options = randomOrderOptions() + const payload = buildOrderPayload({ marker, index, profile, master, product, packageCount, options, productImageFileId }) const createResponse = await request(gateway, timeoutMs, 'POST', '/api/merchant/v1/orders/first-leg', { token, @@ -401,11 +426,16 @@ export async function main(argv = process.argv.slice(2)) { channelCode: master.channel.channelCode, channelName: master.channel.channelName, pricingTaxMode: master.pricingTaxMode, + tradeTerm: options.tradeTerm, + pickupMethod: options.pickupMethod, + taxMode: options.taxMode, + customsInspection: options.customsInspection, + expectedShipDate: payload.expectedShipDate, cargoName: product.cargoName, packageCount, } orders.push(result) - console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} logistics=${result.channelCode}/${result.pricingTaxMode} destination=${result.destinationWarehouseCode} product=${result.cargoName} cartons=${result.packageCount} status=PENDING_REVIEW`) + console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} logistics=${result.channelCode}/${result.pricingTaxMode} destination=${result.destinationWarehouseCode} pickup=${result.pickupMethod} tax=${result.taxMode} inspection=${result.customsInspection} product=${result.cargoName} cartons=${result.packageCount} status=PENDING_REVIEW`) } const report = { diff --git a/scripts/pda-final-complete-diagnostic.mjs b/scripts/pda-final-complete-diagnostic.mjs new file mode 100644 index 000000000..785837ff9 --- /dev/null +++ b/scripts/pda-final-complete-diagnostic.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import process from 'node:process' + +const REQUIRED_EVIDENCE = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'] +const value = (name, fallback = '') => { + const prefix = `--${name}=` + const inline = process.argv.find((item) => item.startsWith(prefix)) + if (inline) return inline.slice(prefix.length) + const index = process.argv.indexOf(`--${name}`) + return index >= 0 ? process.argv[index + 1] || fallback : fallback +} +const gateway = value('gateway', process.env.ADMIN_TEST_GATEWAY || 'http://8.163.30.128:9000').replace(/\/$/, '') +const username = value('username', process.env.ADMIN_TEST_USERNAME || 'platform_admin') +const password = value('password', process.env.ADMIN_TEST_PASSWORD) +const taskIds = value('task-ids').split(',').map((item) => item.trim()).filter(Boolean) +const seedEvidence = process.argv.includes('--seed-evidence') +const stamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14) +const marker = `PDA-FINAL-DIAG-${stamp}` +const output = resolve(value('output', `reports/pda-final-complete-diagnostic-${stamp}.json`)) + +if (!password || taskIds.length === 0) { + throw new Error('required: ADMIN_TEST_PASSWORD (or --password) and --task-ids') +} + +let token = '' +const calls = [] +async function call(method, path, body, headers = {}) { + let response + try { + response = await fetch(`${gateway}${path}`, { + method, + headers: { + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), + ...headers, + }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: AbortSignal.timeout(60_000), + }) + const raw = await response.text() + let payload + try { payload = raw ? JSON.parse(raw) : null } catch { payload = { raw: raw.slice(0, 1000) } } + const result = { + method, path, ...(body === undefined ? {} : { body }), http: response.status, + code: payload?.code == null ? null : String(payload.code), message: payload?.message || null, + requestId: payload?.requestId || response.headers.get('x-request-id'), + traceId: payload?.traceId || response.headers.get('x-trace-id'), data: payload?.data ?? null, + } + calls.push(result) + return result + } catch (error) { + const result = { method, path, ...(body === undefined ? {} : { body }), http: 0, code: null, + message: String(error), requestId: null, traceId: null, data: null } + calls.push(result) + return result + } +} +async function uploadEvidence(tenantId, inboundId, evidenceType) { + const path = `/api/platform/v1/files/loading-evidence/${encodeURIComponent(tenantId)}/${encodeURIComponent(inboundId)}?evidenceType=${encodeURIComponent(evidenceType)}` + const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130]) + const fileName = `${marker}-${inboundId}-${evidenceType.toLowerCase()}.png` + const form = new FormData() + form.append('file', new Blob([png], { type: 'image/png' }), fileName) + try { + const response = await fetch(`${gateway}${path}`, { + method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, + body: form, signal: AbortSignal.timeout(60_000), + }) + const raw = await response.text() + let payload + try { payload = raw ? JSON.parse(raw) : null } catch { payload = { raw: raw.slice(0, 1000) } } + const result = { method: 'POST', path, body: { fileName, evidenceType }, http: response.status, + code: payload?.code == null ? null : String(payload.code), message: payload?.message || null, + requestId: payload?.requestId || response.headers.get('x-request-id'), + traceId: payload?.traceId || response.headers.get('x-trace-id'), data: payload?.data ?? null } + calls.push(result) + return result + } catch (error) { + const result = { method: 'POST', path, body: { fileName, evidenceType }, http: 0, code: null, + message: String(error), requestId: null, traceId: null, data: null } + calls.push(result) + return result + } +} +const ok = (response) => response?.http === 200 && response?.code === '200' +const id = (input) => input == null ? '' : String(input) + +const login = await call('POST', '/api/auth/login', { username, password }) +login.body = { username, password: '[REDACTED]' } +if (!ok(login) || !login.data?.accessToken) { + login.data = null + const failedReport = { marker, startedAt: new Date().toISOString(), gateway, taskIds, stopped: true, + fatal: `login failed: HTTP=${login.http}, code=${login.code}, message=${login.message || 'missing'}`, + results: [], calls } + await mkdir(dirname(output), { recursive: true }) + await writeFile(output, `${JSON.stringify(failedReport, null, 2)}\n`, 'utf8') + console.log(JSON.stringify({ output, marker, stopped: true, fatal: failedReport.fatal }, null, 2)) + process.exit(1) +} +token = login.data.accessToken +login.data = { authenticated: true } + +const pendingResponse = await call('GET', '/api/platform/v1/first-leg/pda-loading/tasks/pending') +const pendingRows = ok(pendingResponse) && Array.isArray(pendingResponse.data) ? pendingResponse.data : [] +const results = [] +let stopped = false + +for (const taskId of taskIds) { + if (stopped) break + const pending = pendingRows.find((item) => id(item?.taskId) === taskId) || null + const warehouseId = id(pending?.warehouseId) + const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}` + const detailResponse = warehouseId + ? await call('GET', `${taskPath}?warehouseId=${encodeURIComponent(warehouseId)}`) + : { http: 0, code: null, message: 'task missing from pending list; warehouseId unavailable', data: null } + const detail = detailResponse.data + const planId = id(detail?.loadingPlanId || pending?.loadingPlanId) + const planPath = `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}` + const planBefore = planId ? await call('GET', planPath) : null + const owners = [...new Map((detail?.items || []).map((item) => [ + `${id(item?.tenantId)}:${id(item?.inboundId)}`, + { tenantId: id(item?.tenantId), inboundId: id(item?.inboundId) }, + ])).values()].filter((item) => item.tenantId && item.inboundId) + const inboundBefore = [] + const evidenceBefore = [] + for (const owner of owners) { + inboundBefore.push(await call('GET', + `/api/platform/inbounds/${encodeURIComponent(owner.inboundId)}?tenantId=${encodeURIComponent(owner.tenantId)}`)) + evidenceBefore.push(await call('GET', + `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}`)) + } + const evidenceSeed = [] + if (seedEvidence) { + for (let ownerIndex = 0; ownerIndex < owners.length; ownerIndex += 1) { + const owner = owners[ownerIndex] + const activeTypes = new Set((evidenceBefore[ownerIndex]?.data?.evidences || []) + .filter((item) => item?.status !== 'VOID').map((item) => item?.evidenceType)) + for (const evidenceType of REQUIRED_EVIDENCE.filter((type) => !activeTypes.has(type))) { + const upload = await uploadEvidence(owner.tenantId, owner.inboundId, evidenceType) + const fileId = id(upload.data?.fileId) + if (!ok(upload) || !fileId) { + evidenceSeed.push({ evidenceType, upload, bind: null }) + break + } + const bindPath = `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}` + const bindBody = { expectedVersion: 0, evidenceType, fileId, + fileName: 'client-must-not-override.png', + sealNo: evidenceType === 'SEAL' ? `SEAL-${stamp}` : null, remark: marker } + const bind = await call('POST', bindPath, bindBody, + { 'Idempotency-Key': `${marker}-${owner.inboundId}-${evidenceType}`.slice(0, 128) }) + evidenceSeed.push({ evidenceType, upload, bind }) + if (!ok(bind)) break + } + } + } + const evidenceCurrent = [] + for (const owner of owners) { + evidenceCurrent.push(await call('GET', + `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}`)) + } + const unresolved = (detail?.items || []).filter((item) => ['PENDING', 'PICKING'].includes(item?.itemStatus)) + const evidenceReady = evidenceCurrent.length > 0 && evidenceCurrent.every((response) => { + const activeTypes = new Set((response.data?.evidences || []) + .filter((item) => item?.status !== 'VOID').map((item) => item?.evidenceType)) + return ok(response) && REQUIRED_EVIDENCE.every((type) => activeTypes.has(type)) + }) + const ready = ok(pendingResponse) && ok(detailResponse) && pending + && pending.workflowStatus === 'AWAITING_REVIEW' + && detail?.workflowStatus === 'AWAITING_REVIEW' + && Number(pending?.pendingItemCount) === 0 && unresolved.length === 0 + && Number.isInteger(Number(detail?.version)) && evidenceReady + const item = { + taskId, pending, detail: detail ? { + id: detail.id, version: detail.version, workflowStatus: detail.workflowStatus, + taskStatus: detail.taskStatus, inboundId: detail.inboundId, loadingPlanId: detail.loadingPlanId, + containerBookingId: detail.containerBookingId, + items: (detail.items || []).map((row) => ({ id: row.id, tenantId: row.tenantId, + inboundId: row.inboundId, itemStatus: row.itemStatus, plannedPackageCount: row.plannedPackageCount, + pickedPackageCount: row.pickedPackageCount, version: row.version })), + } : null, + owners, evidenceReady, evidenceSeed: evidenceSeed.map((entry) => ({ evidenceType: entry.evidenceType, + upload: entry.upload ? { http: entry.upload.http, code: entry.upload.code, requestId: entry.upload.requestId, + traceId: entry.upload.traceId, fileId: entry.upload.data?.fileId || null } : null, + bind: entry.bind ? { http: entry.bind.http, code: entry.bind.code, requestId: entry.bind.requestId, + traceId: entry.bind.traceId } : null })), ready, complete: null, + } + if (!ready) { + item.blocker = `pending/detail/evidence not ready; pendingWorkflow=${pending?.workflowStatus || 'missing'}, detailWorkflow=${detail?.workflowStatus || 'missing'}, pendingItemCount=${pending?.pendingItemCount ?? 'missing'}, unresolved=${unresolved.length}, evidenceReady=${evidenceReady}` + results.push(item) + stopped = true + continue + } + const body = { + expectedTaskVersion: Number(detail.version), + operationId: `${marker}-${taskId}`.slice(0, 128), remark: marker, + } + const complete = await call('POST', `${taskPath}/complete`, body, + { 'Idempotency-Key': `${marker}-${taskId}`.slice(0, 128) }) + item.complete = complete + item.taskAfter = await call('GET', `${taskPath}?warehouseId=${encodeURIComponent(warehouseId)}`) + item.planAfter = await call('GET', planPath) + item.inboundAfter = [] + for (const owner of owners) { + item.inboundAfter.push(await call('GET', + `/api/platform/inbounds/${encodeURIComponent(owner.inboundId)}?tenantId=${encodeURIComponent(owner.tenantId)}`)) + } + results.push(item) + if (!ok(complete)) stopped = true +} + +const report = { marker, startedAt: new Date().toISOString(), gateway, taskIds, seedEvidence, stopped, results, + calls: calls.map((entry) => entry.path === '/api/auth/login' ? { ...entry, data: { authenticated: true } } : entry) } +await mkdir(dirname(output), { recursive: true }) +await writeFile(output, `${JSON.stringify(report, null, 2)}\n`, 'utf8') +console.log(JSON.stringify({ output, marker, stopped, results: results.map((item) => ({ + taskId: item.taskId, ready: item.ready, blocker: item.blocker || null, + complete: item.complete ? { http: item.complete.http, code: item.complete.code, + message: item.complete.message, requestId: item.complete.requestId, traceId: item.complete.traceId } : null, +})) }, null, 2))