From 5086de23a7231a17da7fa3e3779a701dc0ea73b5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 8 Sep 2026 23:56:54 +0800 Subject: [PATCH] fix(acceptance): close overseas dropship pallet chain --- .../service/ReceivingContainerService.java | 2 +- .../move/service/PdaInventoryMoveService.java | 2 +- .../service/PdaStocktakeService.java | 2 +- ...ReceivingContainerServiceBehaviorTest.java | 2 +- .../service/PdaInventoryMoveServiceTest.java | 8 ++--- .../service/PdaStocktakeServiceTest.java | 2 +- scripts/admin-test-env-acceptance.mjs | 4 +-- .../admin-test-env-modules/phase3-loading.mjs | 33 +++++++++++++++++-- .../phase3-producer-chain-contract.test.mjs | 3 ++ ...s-wallet-and-pallet-cost-contract.test.mjs | 1 + ...phase4-overseas-wallet-and-pallet-cost.mjs | 4 ++- ...verseas-acceptance-entry-contract.test.mjs | 2 ++ 12 files changed, 50 insertions(+), 15 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java index 3dd5353c4..54bae8773 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java @@ -344,7 +344,7 @@ public class ReceivingContainerService { if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) { throw conflict("FBA destination must use bulk count"); } - WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode()); + WarehouseResourceGateway.WarehousePalletReference resource = warehouse.inspectPallet(request.palletCode()); validatePallet(ref, resource); InboundPlanPallet pallet = choosePallet(ref, resource); workflow.validateCartonResolved(customerId, ref.plan(), ref.line(), pallet, request.cartonMark()); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java index 85e274203..f5d5263f9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java @@ -50,7 +50,7 @@ public class PdaInventoryMoveService { PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key); if (existing != null) return replay(existing, requestHash); - var pallet = warehouse.resolvePallet(required(request.sourcePalletCode(), "Source pallet code is required")); + var pallet = warehouse.inspectPallet(required(request.sourcePalletCode(), "Source pallet code is required")); if (pallet == null) throw notFound("Source pallet was not found"); if (pallet.warehouseId() != warehouseId) throw conflict("Source pallet belongs to another overseas warehouse"); var target = warehouse.resolveLocation(warehouseId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java index d23ee51b9..1da30169f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java @@ -81,7 +81,7 @@ public class PdaStocktakeService { long resourceId; List candidates; if ("PALLET".equals(scopeType)) { - var pallet = warehouse.resolvePallet(scopeCode); + var pallet = warehouse.inspectPallet(scopeCode); if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse"); resourceId = pallet.palletId(); scopeCode = pallet.palletNo(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java index 0a72f267c..0f944f4b4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java @@ -399,7 +399,7 @@ class ReceivingContainerServiceBehaviorTest { org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow); stubCurrent("OVERSEAS", "CONT-A"); when(cartons.selectOne(any())).thenReturn(carton(1L, "BOX-1")); - when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, null)); + when(warehouse.inspectPallet("PAL-1")).thenReturn(resource(100L, null)); when(pallets.selectList(any())).thenReturn(List.of(planPallet())); doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "duplicate carton")) .when(workflow).validateCartonResolved(any(), any(), any(), any(), any()); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java index 00acb9c97..7c05ae738 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java @@ -61,7 +61,7 @@ class PdaInventoryMoveServiceTest { @Test void atomicallyRelocatesPhysicalPalletAndEveryWarehouseScopedPosition() { String targetLocationCode = "PDAOPS-ADMIN-API-20260825095854036-MOVE"; - when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 20L, 2)); + when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(900L, 20L, 2)); when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01")); when(warehouse.resolveLocation(900L, targetLocationCode)).thenReturn(location(21L, targetLocationCode)); InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100"); @@ -101,7 +101,7 @@ class PdaInventoryMoveServiceTest { @Test void rejectsCrossWarehousePalletWithoutMovingResources() { - when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(901L, 20L, 2)); + when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(901L, 20L, 2)); assertThatThrownBy(() -> service.move(900L, "operator-1", new PdaInventoryMoveRequest("PAL-1", "B-01", 2, "move-1", "库位优化"))) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("409"); @@ -110,7 +110,7 @@ class PdaInventoryMoveServiceTest { @Test void missingSourcePalletIsReportedAsNotFound() { - when(warehouse.resolvePallet("MISSING")).thenReturn(null); + when(warehouse.inspectPallet("MISSING")).thenReturn(null); assertThatThrownBy(() -> service.move(900L, "operator-1", new PdaInventoryMoveRequest("MISSING", "B-01", 2, "move-1", "库位优化"))) @@ -120,7 +120,7 @@ class PdaInventoryMoveServiceTest { @Test void resumesWhenWarehouseCommittedButLastlegTransactionRolledBack() { - when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 21L, 3)); + when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(900L, 21L, 3)); when(warehouse.resolveLocation(900L, "B-01")).thenReturn(location(21L, "B-01")); when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01")); InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java index 51b08108c..5f298b05c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java @@ -90,7 +90,7 @@ class PdaStocktakeServiceTest { @Test void rejectsPalletFromAnotherWarehouseBeforeReadingInventory() { - when(warehouse.resolvePallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( + when(warehouse.inspectPallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( 30L, "PAL-1", 901L, 20L, "OCCUPIED", null, 2)); assertThatThrownBy(() -> service.create(900L, "operator-1", diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index e935bf781..44b83dfbe 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -59,11 +59,11 @@ const MODULE_DEPENDENCIES = new Map([ ['truck-appointments', ['merchant-qualification-fixture']], ['service-center-flow', ['ocean-bookings', 'truck-appointments']], ['phase3-loading-finalize', ['phase3-loading', 'phase3-evidence-gate']], - ['phase3-container-documents', ['phase3-loading-finalize']], + ['phase3-container-documents', ['phase3-outbound-dispatch']], ['phase3-customs', ['phase3-loading-finalize']], ['phase3-container-customs', ['merchant-qualification-fixture']], ['phase3-port-schedules', ['ocean-bookings']], - ['phase3-outbound-dispatch', ['phase3-container-documents', 'phase3-customs', 'phase3-port-schedules', 'service-center-flow']], + ['phase3-outbound-dispatch', ['phase3-customs', 'phase3-port-schedules', 'service-center-flow']], ['phase3-outbound-aggregate', ['phase3-outbound-dispatch']], ['phase3-dispatch-gate', ['phase3-outbound-dispatch']], ['phase3-waiting-for-loading', ['phase3-outbound-dispatch']], diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 858e58340..119ce4a71 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -134,6 +134,20 @@ function inventorySignature(snapshot) { }))) } +function channelGroup(channelCode) { + const value = String(channelCode || '').toUpperCase() + if (value.startsWith('EXX_')) return 'EXX' + if (value.startsWith('MATSON_CLX_')) return 'MATSON_CLX' + if (value.startsWith('MATSON_MAX_')) return 'MATSON_MAX' + if (value.startsWith('HEDE_')) return 'HEDE' + if (value.startsWith('OA_LA_') || value === 'OA_PARCEL') return 'OA_LA' + if (value.startsWith('OA_CHICAGO_')) return 'OA_CHICAGO' + if (value.startsWith('OA_HOUSTON_')) return 'OA_HOUSTON' + if (value.startsWith('OA_SAVANNAH_')) return 'OA_SAVANNAH' + if (value === 'OA_NEW_YORK_TRUCK') return value + throw new Error(`unsupported smart-loading channel group: ${channelCode}`) +} + function operationsSignature(operations) { return JSON.stringify((operations || []).map((operation) => ({ id: id(operation?.id), operationId: operation?.operationId || null, @@ -225,12 +239,25 @@ export async function run(context) { const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS) const inboundCandidates = Array.isArray(candidates) ? candidates.filter((item) => id(item.inboundId) === inboundId && Number(item.availablePackageCount) > 0) : [] - const candidate = inboundCandidates[0] + let candidate = inboundCandidates[0] + let dropshipCandidates = [] + if (source?.fulfillmentType === 'ONE_CLICK_DROPSHIP') { + const group = channelGroup(initial?.summary?.channelCode) + const candidatePath = `${smartLoadingBase}/inbound-candidates?channelGroupCodes=${encodeURIComponent(group)}&warehouseId=${encodeURIComponent(warehouseId)}&customerId=${encodeURIComponent(tenantId)}&inboundCode=${encodeURIComponent(initial?.summary?.inboundCode || source?.inboundCode || '')}&orderStatuses=WAIT_SPLIT&page=1&size=20` + const palletCandidatesResponse = await context.request('GET', candidatePath) + const palletCandidatePage = context.apiSuccess(MODULE, '查询一件代发整托装柜候选', palletCandidatesResponse, '', OPTIONS) + dropshipCandidates = Array.isArray(palletCandidatePage?.records) + ? palletCandidatePage.records.filter((item) => id(item.inboundId) === inboundId + && id(item.palletId) && item.unitType === 'SEALED_PALLET' && item.indivisible === true) : [] + candidate = dropshipCandidates[0] + context.assert(MODULE, '一件代发候选只按已封托托盘返回', palletCandidatesResponse, + dropshipCandidates.length > 0, `inboundId=${inboundId}, pallets=${dropshipCandidates.length}`, OPTIONS) + } context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse, Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId && id(candidate?.warehouseId) === warehouseId, `candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS) - if (!candidate?.inboundDetailId || !warehouseId) return + if ((!candidate?.inboundDetailId && !id(candidate?.palletId)) || !warehouseId) return const loadingPlanResponse = await request(context, 'GET', @@ -240,7 +267,7 @@ export async function run(context) { if (!loadingPlanId) return const dropshipAssignment = source?.fulfillmentType === 'ONE_CLICK_DROPSHIP' const initialAssignmentCount = dropshipAssignment - ? inboundCandidates.filter((item) => id(item.palletId) === id(candidate.palletId)) + ? dropshipCandidates.filter((item) => id(item.palletId) === id(candidate.palletId)) .reduce((total, item) => total + Number(item.availablePackageCount || 0), 0) : 1 const assignmentPath = dropshipAssignment ? `${smartLoadingBase}/plans/${loadingPlanId}/pallet-assignments` diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index e96b15374..ba48c4721 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -503,6 +503,9 @@ test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING au } assert.match(loading, /expectedBatchOperationIds = dropshipWholePallet[\s\S]*\? \[batchKey\][\s\S]*: \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/) assert.match(loading, /dropshipAssignment[\s\S]*pallet-assignments/) + assert.match(loading, /inbound-candidates\?channelGroupCodes=/) + assert.match(loading, /unitType === 'SEALED_PALLET'/) + assert.match(loading, /indivisible === true/) assert.match(loading, /palletId: id\(candidate\.palletId\)/) assert.doesNotMatch(loading, /bridge-assign-inbound/) assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs index 6e565a419..8273d3f5a 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs @@ -35,6 +35,7 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:', 'sourceInboundOrderId:', + 'destinationFbaWarehouseCode: source.destinationFbaWarehouseCode', 'cartonMark:', 'fbaCartonMark:', ]) assert.ok(source.includes(token), token) diff --git a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs index 575b45171..f5b32e584 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs @@ -183,7 +183,8 @@ function relabelPayload(context, warehouseId, source, suffix, chargeAmount) { totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128), newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker, cartonMark: `CM-${value}-${suffix}`.slice(0, 255), - fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255) }], + fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255), + destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }], } } @@ -195,6 +196,7 @@ async function relabelFlow(context, overseasToken, warehouseId, suffix, terminal ? candidatesResponse.body.data : [] const source = candidates.find((candidate) => id(candidate.inboundOrderId) && id(candidate.inventoryBalanceId) + && id(candidate.destinationFbaWarehouseCode) && candidate.positions?.some((position) => id(position.positionId) && Number(position.availablePackages) > 0)) if (source) source.position = source.positions.find((position) => id(position.positionId) && Number(position.availablePackages) > 0) diff --git a/scripts/phase4-overseas-acceptance-entry-contract.test.mjs b/scripts/phase4-overseas-acceptance-entry-contract.test.mjs index 83031a80b..2e54c5144 100644 --- a/scripts/phase4-overseas-acceptance-entry-contract.test.mjs +++ b/scripts/phase4-overseas-acceptance-entry-contract.test.mjs @@ -165,6 +165,8 @@ test('PDA entry expands the real phase-three producer chain and builds an FBA/OV const variant = source.slice(source.indexOf('async runPhase4ProducerVariant'), source.indexOf('async ensurePhase4PdaProducerMatrix')) assert.ok(variant.indexOf('phase3-outbound-dispatch-phase4-') < variant.indexOf('phase3-container-documents-phase4-')) assert.ok(variant.indexOf('phase3-container-documents-phase4-') < variant.indexOf('phase3-carrier-schedule-queries-phase4-')) + assert.match(source, /\['phase3-container-documents', \['phase3-outbound-dispatch'\]\]/) + assert.doesNotMatch(source, /\['phase3-outbound-dispatch', \['phase3-container-documents'/) assert.match(source, /carrierScheduleQueryId/) assert.match(source, /readyForPhase4Pda === true/) assert.equal(modules.indexOf('phase4-overseas-pda-operations'), modules.indexOf('phase4-overseas-pda') + 1)