diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java index 7de2bcc20..434f24fc9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java @@ -52,11 +52,12 @@ public interface DispatchCandidateMapper { " / NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0) ELSE NULL END AS grossWeightKg," + "MIN(ip.container_no) AS containerNo," + CONFIRMED_APPOINTMENT_AT + " AS appointmentAt,NULL AS expectedLoadingAt," + - "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + + // 仓ID文本统一保留128字符,避免混合CASE的NONE排序规则及无长度CHAR截断;来源优先级与身份等值不变。 + "CAST(CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + "FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " + - "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," + + "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')," + "(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " + - "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," + + "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS CHAR(128)) AS overseasWarehouseId," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java index f4b40f5d8..daf2c0e44 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java @@ -16,6 +16,57 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; class DispatchCandidateMapperContractTest { + /** 执行正式仓ID的完整CASE及外层过滤,保留实库存优先、客户来源和长ID文本身份。 */ + @Test + void wholeCandidateWarehouseProjectionExecutesAsTextWithoutChangingSourceOwnership() throws Exception { + String sql = String.join(" ", DispatchCandidateMapper.class.getMethod("wholeAvailable", String.class, + String.class, List.class, String.class, String.class, String.class, String.class).getAnnotation(Select.class).value()); + int start = sql.indexOf("CAST(CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id)"); + int end = sql.indexOf(" AS overseasWarehouseId", start); + assertTrue(start >= 0 && end > start, "正式whole候选必须在整个仓CASE外统一为文本投影"); + String expression = sql.substring(start, end).replace(">", ">").replace("<", "<"); + assertTrue(expression.endsWith("END AS CHAR(128))")); + assertTrue(expression.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')")); + String warehouseFilter = sql.substring(sql.lastIndexOf(") selected WHERE ") + 1) + .replace("#{warehouseId}", "?").replace("", ""); + String query = "SELECT selected.planPalletId FROM (SELECT p.id AS planPalletId," + expression + + " AS overseasWarehouseId FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id)" + + warehouseFilter; + try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-whole-warehouse-text;MODE=MySQL")) { + var statement = connection.createStatement(); + statement.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id BIGINT,source_order_id BIGINT)"); + statement.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT)"); + statement.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR(128),pallet_id BIGINT,available_packages BIGINT,overseas_warehouse_id VARCHAR(128))"); + statement.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id VARCHAR(128))"); + statement.execute("INSERT INTO ll_inbound_plan VALUES (1,'A',193000000000200002,NULL)," + + "(2,'A',193000000000100001,NULL),(3,'B',NULL,20),(4,'A',193000000000100001,NULL)," + + "(5,'B',193000000000200002,NULL),(6,'A',NULL,NULL),(7,'A',193000000000100001,21)," + + "(8,'A',193000000000100001,NULL),(9,'A',NULL,21)"); + statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES (1,1,10),(2,2,NULL),(3,3,NULL)," + + "(4,4,40),(5,5,50),(6,6,NULL),(7,7,NULL),(8,8,80),(9,9,NULL)"); + statement.execute("INSERT INTO ll_inventory_position VALUES ('A',10,1,'193000000000100001')," + + "('B',40,1,'193000000000100001'),('A',40,0,'193000000000100001')," + + "('B',50,1,'193000000000100001'),('A',80,1,'193000000000200002')"); + statement.execute("INSERT INTO ll_order VALUES (20,'A','193000000000200002')," + + "(20,'B','193000000000100001'),(21,'B','193000000000100001')"); + try (var prepared = connection.prepareStatement(query)) { + assertWarehouseRows(prepared, "193000000000100001", List.of(1L, 2L, 3L, 5L, 7L)); + assertWarehouseRows(prepared, "193000000000200002", List.of(8L)); + for (String invalid : List.of("19300000000010000", "193000000000100001x", "0193000000000100001", "1.93e17", "wrong")) + assertWarehouseRows(prepared, invalid, List.of()); + } + } + } + + /** 每次实际执行同一参数化仓过滤,禁止用前缀、数字强转或其他客户库存替代身份。 */ + private void assertWarehouseRows(java.sql.PreparedStatement prepared, String warehouse, List expected) throws Exception { + prepared.setString(1, warehouse); + var actual = new ArrayList(); + try (var rows = prepared.executeQuery()) { while (rows.next()) actual.add(rows.getLong(1)); } + actual.sort(Long::compareTo); + assertEquals(expected, actual, warehouse); + } + @Test void ordinaryAndReturnRelabelCandidatesExcludeDirectOutboundDeliveryModes() throws Exception { for (String method : List.of("relabelAvailable", "nativeReturnRelabelAvailable")) { @@ -200,7 +251,9 @@ class DispatchCandidateMapperContractTest { String sql = String.join(" ", method.getAnnotation(Select.class).value()); assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL")); assertTrue(sql.contains("pos.overseas_warehouse_id")); - assertTrue(sql.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')")); + assertTrue(sql.contains(method.getName().equals("available") + ? "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')" + : "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')")); assertTrue(sql.contains("source_order.id=ip.source_order_id")); assertTrue(sql.contains("source_order.customer_id=ip.customer_id")); assertTrue(sql.contains("source_order.overseas_warehouse_id")); diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs index de74c0f17..699e21fc4 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs @@ -76,10 +76,14 @@ function model(fault = '', countPatch = {}) { if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : merchant.id }) if (path === '/api/merchant/v1/last-leg/warehouse-transfers') { assert.ok(merchant); assert.equal(body.inboundCode, undefined); assert.deepEqual(body.sourceLastlegOrderIds, []) - assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1) + assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1); assert.equal(body.items[0].quantity, 1) const transferNo = `IN${transfers.length}` + const itemId = `FORECAST-ITEM${transfers.length}` const transfer = { transferNo, inboundCode: transferNo, customerId: merchant.id, operatingWarehouseId: body.operatingWarehouseId, - status: 'DRAFT', pallets: [] } + status: 'DRAFT', packageCount: 1, pallets: [], + items: [{ ...copy(body.items[0]), id: itemId, ...(fault === 'missing declared quantity' ? { quantity: null } : {}) }], + cartons: fault === 'missing forecast carton' ? [] : [{ cartonMark: `BX-FORECAST${transfers.length}`, status: 'FORECAST', + transferItemId: fault === 'foreign forecast item' ? 'OTHER-ITEM' : itemId }] } transfers.push(transfer); return response(transfer) } if (path.includes('/warehouse-transfers/')) { @@ -88,10 +92,39 @@ function model(fault = '', countPatch = {}) { if (path.startsWith('/api/merchant/')) assert.equal(transfer.customerId, merchant.id) if (path.endsWith('/submit')) transfer.status = 'PENDING_REVIEW' else if (path.endsWith('/review')) { assert.equal(url.searchParams.get('customerId'), transfer.customerId); assert.equal(body.decision, 'APPROVE'); transfer.status = 'WAIT_PDA_RECEIVING' } - else if (path.endsWith('/pallets/drafts')) transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: body.cartonCount }] + else if (path.endsWith('/pallets/drafts')) { + // The actual PalletDraft DTO accepts identity only; never derive measurement from ignored request fields. + assert.deepEqual(Object.keys(body), ['idempotencyKey']) + transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: 0, lengthCm: 0, widthCm: 0, heightCm: 0, volumeCbm: 0, grossWeightKg: 0 }] + } else if (path.endsWith('/generate')) Object.assign(transfer.pallets[0], { status: 'GENERATED', warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}` }) else if (path.endsWith('/bind')) { assert.equal(body.palletCode, transfer.pallets[0].palletNo); transfer.pallets[0].status = 'BOUND' } - else if (path.endsWith('/palletize')) { transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' } + else if (path.endsWith('/cartons/count')) { + assert.equal(method, 'POST'); assert.equal(transfer.pallets[0].status, 'BOUND') + assert.equal(transfer.pallets[0].cartonCount, 0); assert.equal(body.forecastPackageCount, transfer.packageCount) + assert.equal(body.boxes.length, 1) + const box = body.boxes[0], item = transfer.items[0], carton = transfer.cartons[0], pallet = transfer.pallets[0] + assert.equal(box.cartonMark, carton.cartonMark); assert.equal(box.transferItemId, item.id) + assert.equal(box.transferPalletId, pallet.id); assert.notEqual(box.transferPalletId, pallet.warehousePalletId) + assert.deepEqual([box.length, box.width, box.height, box.weight], [100, 100, 100, 100]) + assert.deepEqual(box.items, [{ skuCode: item.skuCode, quantity: item.quantity }]) + Object.assign(carton, { transferPalletId: pallet.id, status: 'COUNTED', countedAt: '2026-10-20T09:00:00', + length: box.length, width: box.width, height: box.height, weight: box.weight, quantity: 1, + items: [{ transferItemId: item.id, skuCode: item.skuCode, quantity: 1 }] }) + Object.assign(pallet, { cartonCount: 1, volumeCbm: 1, grossWeightKg: 100 }) + if (fault === 'uncounted carton') { carton.status = 'FORECAST'; carton.countedAt = null } + if (fault === 'wrong counted pallet') carton.transferPalletId = 'OTHER-PALLET' + if (fault === 'wrong counted sku') carton.items[0].skuCode = 'OTHER-SKU' + if (fault === 'missing carton measurement') carton.weight = null + if (fault === 'zero counted pallet') pallet.cartonCount = 0 + if (fault === 'wrong measured pallet weight') pallet.grossWeightKg = 0 + } + else if (path.endsWith('/palletize')) { + assert.equal(transfer.cartons.length, 1); assert.equal(transfer.cartons[0].status, 'COUNTED'); assert.ok(transfer.cartons[0].countedAt) + assert.equal(transfer.pallets[0].cartonCount, 1); assert.equal(transfer.pallets[0].grossWeightKg, 100) + assert.equal(transfer.pallets[0].volumeCbm, 1); assert.ok(body.photoFileIds.length) + transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' + } else if (path.endsWith('/confirm-measurement')) transfer.status = 'READY_FOR_DISPATCH' else assert.equal(method, 'GET') return response(transfer) @@ -182,13 +215,26 @@ test('real helpers build independent current-run customers, receipts and one com assert.equal(proof.status, 'READY'); assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT']) assert.equal(m.trip.dispatches.length, 1); assert.equal(new Set(fixture.members.map(row => row.dispatchNo)).size, 1); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA']) assert.equal(m.calls.filter(row => row.method === 'POST' && row.raw === '/api/platform/v1/last-leg/dispatches').length, 1) + assert.equal(m.calls.filter(row => row.raw.endsWith('/cartons/count')).length, 2) + for (const member of fixture.members) { + const prefix = `/api/pda/v1/last-leg/warehouse-transfers/${member.incomingNo}` + const draft = m.calls.findIndex(row => row.raw === `${prefix}/pallets/drafts`) + const bind = m.calls.findIndex(row => row.raw.startsWith(`${prefix}/pallets/`) && row.raw.endsWith('/bind')) + const count = m.calls.findIndex(row => row.raw === `${prefix}/cartons/count`) + const photo = m.calls.findIndex(row => row.raw === `${prefix}/measurement-photos/files`) + const receipt = m.calls.findIndex(row => row.raw === `${prefix}/palletize`) + assert.ok(draft < bind && bind < count && count < photo && photo < receipt) + assert.equal(member.measurementReceipt.countedCarton.cartonMark, m.calls[count].body.boxes[0].cartonMark) + assert.equal(member.measurementReceipt.syntheticTestMaterials, true) + assert.equal(member.measurementReceipt.finalizedPallet.cartonCount, 1) + } assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 0) assert.ok(!m.calls.some(row => /\/pick$|\/depart$|\/bookings|\/capacity-plan/.test(row.raw))) const persisted = JSON.stringify(m.context.createdResources) for (const secret of ['caller-pda-secret', 'admin-secret', 'fresh-secret-', 'merchant-100-secret', 'refresh-100-secret']) assert.ok(!persisted.includes(secret)) }) -for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) { +for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) { test(`fail closed, retain partial resources and clean owned sessions: ${fault}`, async () => { const m = model(fault) await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options)) @@ -198,6 +244,8 @@ for (const fault of ['disabled receiving', 'reused customer', 'foreign login', ' assert.equal(logouts.length, logins.length) assert.ok(!m.calls.some(row => row.method === 'DELETE' || row.raw.includes('/users/'))) if (fault === 'disabled receiving') assert.equal(m.calls.filter(row => row.method !== 'GET').length, 0) + if (['missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight'].includes(fault)) + assert.equal(m.calls.some(row => row.raw.endsWith('/palletize') || row.raw === '/api/platform/v1/last-leg/dispatches'), false) const previousCount = m.calls.length await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /already started/) assert.equal(m.calls.length, previousCount) diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs index fea8b2930..e617cea61 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs @@ -151,7 +151,7 @@ export async function createFleetCrossCustomerFixture(context, { execute = false destinationWarehouseCode: destination.warehouse.warehouseCode, destinationWarehouseName: destination.warehouse.warehouseName, deliveryAddress, packageCount: 1, volumeCbm: 1, deliveryMethod: 'CUSTOMER_DELIVERY', estimatedArrivalAt: localTime(1800000), needRelabel: false, remark: member.customerMarker, idempotencyKey: key(`CREATE-${suffix}`), - items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1 }] } + items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1, quantity: 1 }] } let incoming = await request('POST', MERCHANT, createBody, `Create new incoming ${suffix}`, auth.accessToken) member.incomingNo = incoming?.transferNo check('Ordinary transfer creates fresh incoming without borrowed source inventory', member.incomingNo && incoming.inboundCode === member.incomingNo @@ -167,8 +167,14 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('PDA task is untouched and belongs to the requested receiving warehouse', task?.transferNo === member.incomingNo && task.inboundCode === member.incomingNo && id(task.operatingWarehouseId) === id(warehouseId) && task.status === 'WAIT_PDA_RECEIVING' && task.pallets?.length === 0) - const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`), - cartonCount: 1, lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 }, 'Measure one real test pallet', pdaToken) + const forecastItem = task.items?.[0], forecastCarton = task.cartons?.[0] + check('One original forecast carton and declared SKU belong to this fresh one-box incoming', safeFixtureCount(task.packageCount) === 1 + && task.items?.length === 1 && forecastItem?.id && forecastItem.skuCode === createBody.items[0].skuCode + && safeFixtureCount(forecastItem.packageCount) === 1 && safeFixtureCount(forecastItem.quantity) === 1 + && task.cartons?.length === 1 && forecastCarton?.cartonMark + && (forecastCarton.transferItemId == null || id(forecastCarton.transferItemId) === id(forecastItem.id))) + // PalletDraft creates only an empty identity; measurements belong to the real carton-count command below. + const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`) }, 'Create empty TEST pallet draft', pdaToken) check('Measurement created one pallet draft', measured?.pallets?.length === 1 && measured.pallets[0].id && measured.pallets[0].status === 'DRAFT') const palletPath = `${pdaPath}/pallets/${encode(measured.pallets[0].id)}` const generated = await request('POST', `${palletPath}/generate`, { idempotencyKey: key(`GENERATE-${suffix}`) }, 'Generate authoritative warehouse pallet', pdaToken) @@ -176,8 +182,28 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('Generated physical pallet has real references', generated?.pallets?.length === 1 && pallet.status === 'GENERATED' && pallet.warehousePalletId && pallet.palletNo) member.palletId = id(pallet.warehousePalletId); member.palletNo = pallet.palletNo const bound = await request('POST', `${palletPath}/bind`, { palletCode: member.palletNo, idempotencyKey: key(`BIND-${suffix}`) }, 'Bind scanned physical pallet', pdaToken) - check('Bound pallet is exactly the measured one-box pallet', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND' - && id(bound.pallets[0].warehousePalletId) === member.palletId && safeFixtureCount(bound.pallets[0].cartonCount) === 1) + check('Bound pallet is exactly the generated empty physical pallet before carton counting', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND' + && id(bound.pallets[0].id) === id(measured.pallets[0].id) + && id(bound.pallets[0].warehousePalletId) === member.palletId && safeFixtureCount(bound.pallets[0].cartonCount) === 0) + const counted = await request('POST', `${pdaPath}/cartons/count`, { idempotencyKey: key(`COUNT-${suffix}`), + forecastPackageCount: safeFixtureCount(task.packageCount), boxes: [{ cartonMark: forecastCarton.cartonMark, + transferItemId: id(forecastItem.id), transferPalletId: id(measured.pallets[0].id), + length: 100, width: 100, height: 100, weight: 100, + items: [{ skuCode: forecastItem.skuCode, quantity: safeFixtureCount(forecastItem.quantity) }] }] }, + 'Count original TEST carton, record dimensions/SKU and allocate it to the bound pallet', pdaToken) + const countedCarton = counted?.cartons?.[0], countedPallet = counted?.pallets?.[0] + check('Actual carton count proves one declared box/SKU on the original bound pallet', counted?.transferNo === member.incomingNo + && counted.cartons?.length === 1 && countedCarton.cartonMark === forecastCarton.cartonMark + && countedCarton.status === 'COUNTED' && countedCarton.countedAt + && id(countedCarton.transferPalletId) === id(measured.pallets[0].id) + && id(countedCarton.transferItemId) === id(forecastItem.id) + && ['length', 'width', 'height', 'weight'].every(field => Number(countedCarton[field]) === 100) + && safeFixtureCount(countedCarton.quantity) === 1 && countedCarton.items?.length === 1 + && countedCarton.items[0].skuCode === forecastItem.skuCode + && id(countedCarton.items[0].transferItemId) === id(forecastItem.id) && safeFixtureCount(countedCarton.items[0].quantity) === 1 + && counted.pallets?.length === 1 && countedPallet.status === 'BOUND' && id(countedPallet.id) === id(measured.pallets[0].id) + && id(countedPallet.warehousePalletId) === member.palletId && safeFixtureCount(countedPallet.cartonCount) === 1 + && Number(countedPallet.grossWeightKg) === 100 && Number(countedPallet.volumeCbm) === 1) const photoPath = `${pdaPath}/measurement-photos/files` const photo = record('POST', photoPath, undefined, await context.requestMultipart('POST', photoPath, { name: `${member.customerMarker}-TEST-MEASUREMENT.png`, type: 'image/png', content: testImage }, pdaToken), 'Upload TEST measurement evidence') @@ -189,7 +215,7 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('Customer confirmation makes ordinary incoming dispatchable', incoming?.transferNo === member.incomingNo && incoming.status === 'READY_FOR_DISPATCH') member.receipt = await stock(member) member.balanceId = id(member.receipt.balance.id); member.positionId = id(member.receipt.positions[0].id); member.receiptVerified = true - member.measurementReceipt = { sourceBusinessNo: member.incomingNo, syntheticTestMaterials: true, + member.measurementReceipt = { sourceBusinessNo: member.incomingNo, syntheticTestMaterials: true, countedCarton, sourceType: 'WAREHOUSE_TRANSFER_PDA', finalizedPallet: incoming.pallets?.find(row => id(row.warehousePalletId) === member.palletId) } const candidates = await request('GET', `${ROOT}/dispatches/candidates?${new URLSearchParams({ warehouseId: id(warehouseId), orderNo: member.incomingNo })}`, undefined, 'Read own native received source selection')