From 0c984e8f5f50d22b9f3f6c66a02e04e800daee12 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 8 Sep 2026 23:34:12 +0800 Subject: [PATCH] fix(lastleg): inspect occupied pallets for PDA queries --- .../gateway/HttpWarehouseResourceGateway.java | 21 ++++++++++++---- .../pda/gateway/WarehouseResourceGateway.java | 3 +++ .../service/PdaWarehouseService.java | 2 +- .../HttpWarehouseResourceGatewayTest.java | 18 ++++++++++++++ .../service/PdaWarehouseServiceTest.java | 4 ++-- ...s-wallet-and-pallet-cost-contract.test.mjs | 8 ++++++- ...phase4-overseas-wallet-and-pallet-cost.mjs | 24 +++++++++++++++---- 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java index a5683449d..775fa3fd3 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java @@ -117,6 +117,17 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { @Override public WarehousePalletReference resolvePallet(String palletCode) { + WarehousePalletData data = palletData(palletCode); + requireReservable(data); + return pallet(data); + } + + @Override + public WarehousePalletReference inspectPallet(String palletCode) { + return pallet(palletData(palletCode)); + } + + private WarehousePalletData palletData(String palletCode) { WarehousePalletResponse response = client.get() .uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode) .header("X-Internal-Service-Token", internalToken) @@ -130,10 +141,12 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Warehouse pallet service returned no usable response"); } - requireReservable(response.data()); - return new WarehousePalletReference(response.data().id(), response.data().palletCode(), - response.data().warehouseId(), response.data().locationId(), response.data().status(), - response.data().fbaWarehouseCode(), response.data().unitPrice(), response.data().version()); + return response.data(); + } + + private WarehousePalletReference pallet(WarehousePalletData data) { + return new WarehousePalletReference(data.id(), data.palletCode(), data.warehouseId(), data.locationId(), + data.status(), data.fbaWarehouseCode(), data.unitPrice(), data.version()); } @Override diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java index af7fd8eac..0a299e0c8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java @@ -9,6 +9,9 @@ public interface WarehouseResourceGateway { void scrapPallet(String customerId, long palletId); WarehousePalletReference resolvePallet(String palletCode); WarehousePalletReference resolvePallet(long palletId); + default WarehousePalletReference inspectPallet(String palletCode) { + return resolvePallet(palletCode); + } default WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) { throw new UnsupportedOperationException("Warehouse location resolution is not configured"); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java index f7c70d02b..4c58f9f7e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java @@ -41,7 +41,7 @@ public class PdaWarehouseService { } public PdaWarehouseResponses.Pallet pallet(long warehouseId, PdaWarehouseRequests.Scan request) { - WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.resolvePallet(code(request.code())); + WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.inspectPallet(code(request.code())); if (pallet == null) throw notFound("Warehouse pallet was not found"); if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse"); WarehouseResourceGateway.WarehouseLocationReference location = warehouse.resolveLocation(warehouseId, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java index be03e6fa4..b758ff1ed 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java @@ -64,6 +64,24 @@ class HttpWarehouseResourceGatewayTest { server.verify(); } + @Test + void inspectAllowsOccupiedPalletForReadOnlyInventoryQueries() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-OCCUPIED")) + .andRespond(withSuccess(""" + {"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-OCCUPIED", + "warehouseId":2092040869757231105,"locationId":2092040891076878337, + "status":"OCCUPIED","fbaWarehouseCode":null,"version":4, + "locationType":"STORAGE","locationStatus":"OCCUPIED","reservable":false, + "reservationBlockReason":"PALLET_NOT_IDLE"}} + """, MediaType.APPLICATION_JSON)); + + WarehouseResourceGateway.WarehousePalletReference result = gateway.inspectPallet("PAL-OCCUPIED"); + + assertThat(result.palletId()).isEqualTo(2092040891261427713L); + assertThat(result.status()).isEqualTo("OCCUPIED"); + server.verify(); + } + @Test void resolveFailsClosedWhenWarehouseOmitsReservationFacts() { server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN")) diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java index a487e38b5..4c561ba56 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java @@ -42,7 +42,7 @@ class PdaWarehouseServiceTest { @Test void palletFromAnotherWarehouseIsRejected() { - when(warehouse.resolvePallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( + when(warehouse.inspectPallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( 1L, "PAL-1", 901L, 2L, "OCCUPIED", null, 1)); assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("PAL-1"))) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("409"); @@ -50,7 +50,7 @@ class PdaWarehouseServiceTest { @Test void missingPalletIsReportedAsNotFound() { - when(warehouse.resolvePallet("MISSING")).thenReturn(null); + when(warehouse.inspectPallet("MISSING")).thenReturn(null); assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("MISSING"))) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("404"); 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 e6027aaed..6e565a419 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 @@ -29,13 +29,19 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释 "'RELEASED'", "terminalStatus === 'COMPLETED'", "'CANCELLED'", - 'relabelPayload(context, warehouseId, suffix, 0.01)', + 'relabelPayload(context, warehouseId, source, suffix, 0.01)', + '/relabel-applications/inventory-candidates', + 'operationWarehouseId: id(warehouseId)', + 'sourceInventoryBalanceId:', + 'sourceInventoryPositionId:', + 'sourceInboundOrderId:', 'cartonMark:', 'fbaCartonMark:', ]) assert.ok(source.includes(token), token) assert.match(source, /actualBalance[\s\S]*frozenBalance[\s\S]*availableBalance[\s\S]*creditLimit[\s\S]*creditUsed[\s\S]*availableCredit/) assert.match(source, /voucherFileId: id\(files\.find\(\(item\) => item\.category === 'VOUCHER'\)\?\.fileId\)/) assert.doesNotMatch(source, /voucherFileId:\s*Number\(/) + assert.doesNotMatch(source, /customerOrderNo:[^\n]+overseasWarehouseId:/) assert.doesNotMatch(source, /internal\/finance|jdbc|mysql|process\.env|https?:\/\//i) }) 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 93e37ce3f..575b45171 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 @@ -169,14 +169,17 @@ async function procurementFlow(context, fixture, overseasToken) { return { orderNo, feeNo, freezePollRequests: frozen.attempts, settlePollRequests: settled.attempts } } -function relabelPayload(context, warehouseId, suffix, chargeAmount) { +function relabelPayload(context, warehouseId, source, suffix, chargeAmount) { const value = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase() return { - customerOrderNo: `${context.marker}-${suffix}`.slice(0, 128), overseasWarehouseId: id(warehouseId), + customerOrderNo: `${context.marker}-${suffix}`.slice(0, 128), operationWarehouseId: id(warehouseId), labelDeadline: new Date(Date.now() + 86_400_000).toISOString().slice(0, 19), contactName: '全量验收联系人', contactPhone: '+1-213-555-0199', chargeAmount, remark: context.marker, idempotencyKey: `${context.marker}-${suffix}`.slice(0, 255), - items: [{ skuCode: `OLD-${value}-${suffix}`.slice(0, 128), productNameZh: `${context.marker} 换标商品`, + items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId), + sourceInventoryPositionId: id(source.position.positionId), sourceInboundOrderId: id(source.inboundOrderId), + skuCode: source.skuCode || `OLD-${value}-${suffix}`.slice(0, 128), + productNameZh: source.productNameZh || `${context.marker} 换标商品`, 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), @@ -185,7 +188,20 @@ function relabelPayload(context, warehouseId, suffix, chargeAmount) { } async function relabelFlow(context, overseasToken, warehouseId, suffix, terminalStatus) { - const payload = relabelPayload(context, warehouseId, suffix, 0.01) + const candidatesResponse = await context.requestOverseas('GET', query( + '/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', { operationWarehouseId: warehouseId }), + undefined, {}, overseasToken) + const candidates = success(candidatesResponse) && Array.isArray(candidatesResponse.body?.data) + ? candidatesResponse.body.data : [] + const source = candidates.find((candidate) => id(candidate.inboundOrderId) + && id(candidate.inventoryBalanceId) + && 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) + assert(context, `${suffix}换标读取当前客户权威库存候选`, candidatesResponse, + Boolean(source?.position), `warehouseId=${warehouseId}, candidates=${candidates.length}`) + if (!source?.position) return null + const payload = relabelPayload(context, warehouseId, source, suffix, 0.01) const created = await context.requestOverseas('POST', '/api/merchant/v1/last-leg/relabel-applications', payload, {}, overseasToken) const relabelNo = created?.body?.data?.application?.relabelNo assert(context, `${suffix}换标单创建并携带收费金额`, created,