From 8df0f4e823ece83c33798f656714b2998d1136cb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 14:18:20 +0800 Subject: [PATCH] fix(overseas): repair pallet transitions and transfer queries --- .../gateway/HttpPalletPhysicalGateway.java | 20 +++++++++++--- .../service/WarehouseTransferService.java | 6 +++-- .../HttpPalletPhysicalGatewayTest.java | 26 ++++++++++++++++++ .../service/WarehouseTransferServiceTest.java | 17 ++++++++++++ ...overseas-client-services-contract.test.mjs | 2 +- .../phase4-overseas-client-services.mjs | 8 +++--- ...-overseas-pda-operations-contract.test.mjs | 5 ++-- .../phase4-overseas-pda-operations.mjs | 27 +++++++++---------- ...phase4-overseas-wallet-and-pallet-cost.mjs | 11 +++++++- .../WarehousePalletTransitionService.java | 7 ++--- .../WarehousePalletTransitionServiceTest.java | 16 +++++++++++ 11 files changed, 114 insertions(+), 31 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java index 0a3472298..a47e6234e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java @@ -1,5 +1,7 @@ package com.baiyue.lastleg.palletops.gateway; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; import com.baiyue.lastleg.config.LastlegRestClientFactory; import com.baiyue.lastleg.config.LastlegRestClientProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -111,18 +113,28 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway { .onStatus(HttpStatusCode::isError, (request, result) -> { throw new IllegalStateException("Warehouse rejected pallet transition: HTTP " + result.getStatusCode()); }).body(TransitionEnvelope.class); - if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned no transition"); + requireTransitionSuccess(response); + if (response.data() == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Warehouse returned no transition"); return response.data(); } @Override public void compensate(long warehouseId, Transition transition) { - client.post().uri("/internal/v1/pda-pallet-transitions/{id}/{key}/compensate", + TransitionEnvelope response = client.post().uri("/internal/v1/pda-pallet-transitions/{id}/{key}/compensate", transition.palletId(), transition.operationKey()) .header("X-Internal-Service-Token", internalToken).body(Map.of("warehouseId", warehouseId)) .retrieve().onStatus(HttpStatusCode::isError, (request, result) -> { throw new IllegalStateException("Warehouse rejected pallet compensation: HTTP " + result.getStatusCode()); - }).toBodilessEntity(); + }).body(TransitionEnvelope.class); + requireTransitionSuccess(response); + } + + private void requireTransitionSuccess(TransitionEnvelope response) { + if (response != null && "200".equals(response.code())) return; + if (response != null && "A0409".equals(response.code())) { + throw new BizException(ResultCode.CONFLICT, response.message()); + } + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Warehouse pallet transition failed"); } @JsonIgnoreProperties(ignoreUnknown = true) @@ -130,5 +142,5 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway { @JsonIgnoreProperties(ignoreUnknown = true) private record LocationEnvelope(LocationReference data) { } @JsonIgnoreProperties(ignoreUnknown = true) - private record TransitionEnvelope(Transition data) { } + private record TransitionEnvelope(String code, String message, Transition data) { } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index f4c8b71fe..b4174ee1b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -433,8 +433,10 @@ public class WarehouseTransferService { value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(), Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), - relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(), - relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(), + value.getRelabelNo() == null || relabelApplicationsByNo.get(value.getRelabelNo()) == null + ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(), + value.getRelabelNo() == null || relabelApplicationsByNo.get(value.getRelabelNo()) == null + ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(), value.getStatus(), value.getEstimatedArrivalAt(), value.getCreatedAt())).toList(); return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages()); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java index fbed131ed..f7861ca00 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java @@ -26,6 +26,32 @@ class HttpPalletPhysicalGatewayTest { ReflectionTestUtils.setField(gateway, "internalToken", "test-token"); } + @Test + void applyPreservesBusinessConflictInsteadOfReportingMissingTransition() { + server.expect(requestTo("http://warehouse/internal/v1/pda-pallet-transitions/30")) + .andRespond(withSuccess(""" + {"code":"A0409","message":"Pallet version changed","data":null} + """, MediaType.APPLICATION_JSON)); + var pallet = new PalletPhysicalGateway.PalletReference(30L, "PAL-1", 88L, 40L, "IDLE", "XLX1", 2); + assertThatThrownBy(() -> gateway.apply(88L, pallet, "OCCUPIED", 40L, "key", "swap")) + .isInstanceOf(com.baiyue.common.core.exception.BizException.class) + .hasMessageContaining("Pallet version changed"); + server.verify(); + } + + @Test + void compensationBusinessFailureIsNotReportedAsSuccess() { + server.expect(requestTo("http://warehouse/internal/v1/pda-pallet-transitions/30/key/compensate")) + .andRespond(withSuccess(""" + {"code":"A0409","message":"Pallet changed after operation","data":null} + """, MediaType.APPLICATION_JSON)); + var transition = new PalletPhysicalGateway.Transition("key", 30L, "PAL-1", 88L, + "IDLE", "OCCUPIED", 40L, 40L, 2, 3, "APPLIED", false); + assertThatThrownBy(() -> gateway.compensate(88L, transition)) + .isInstanceOf(com.baiyue.common.core.exception.BizException.class); + server.verify(); + } + @Test void resolveMapsBusinessNotFoundEnvelopeToStableNotFound() { server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/MISSING")) diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 864315768..961ab1fd1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -127,6 +127,23 @@ class WarehouseTransferServiceTest { assertEquals("READY_FOR_DISPATCH", result.records().get(0).relabelFulfillmentStatus()); } + @Test + void transferPageWithoutRelabelReturnsEmptyRelabelStatuses() { + WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING); + transfer.setRelabelNo(null); + Page resultPage = new Page<>(1, 20, 1); + resultPage.setRecords(List.of(transfer)); + when(transfers.selectPage(any(), any())).thenReturn(resultPage); + + var result = service.page("TENANT-A", new WarehouseTransferRequests.Query( + 1, 20, null, null, null, null, null, null, null, null)); + + assertEquals(1, result.records().size()); + assertNull(result.records().get(0).relabelApplicationStatus()); + assertNull(result.records().get(0).relabelFulfillmentStatus()); + verify(relabelApplications, never()).selectList(any()); + } + @Mock WarehouseTransferOrderMapper transfers; @Mock WarehouseTransferItemMapper transferItems; @Mock WarehouseTransferCartonMapper transferCartons; diff --git a/scripts/admin-test-env-modules/phase4-overseas-client-services-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-client-services-contract.test.mjs index ab2b8a5ba..d5d99517b 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-client-services-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-client-services-contract.test.mjs @@ -41,7 +41,7 @@ test('relabel transfer and discard cover create replay conflict list detail and assert.match(source, /query\(`\$\{RELABEL\}\/inventory-candidates`, \{/) for (const token of [ '客户创建贴标申请', '客户贴标申请幂等重放', '贴标同幂等键异载荷返回A0409', - '客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_PROCESSING', + '客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_REVIEW', '客户创建仓储转运申请', '客户仓储转运申请幂等重放', '转运同幂等键异载荷返回A0409', '客户查询自身仓储转运列表', '客户查询自身仓储转运详情', '客户创建货物废弃申请', '客户货物废弃申请幂等重放', diff --git a/scripts/admin-test-env-modules/phase4-overseas-client-services.mjs b/scripts/admin-test-env-modules/phase4-overseas-client-services.mjs index bdb071b5c..29231889c 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-client-services.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-client-services.mjs @@ -129,7 +129,7 @@ async function verifyRelabel(context, token, order) { const created = await merchant(context, token, 'POST', '客户创建贴标申请', RELABEL, body) const relabelNo = id(created.data?.application?.relabelNo) context.assert(MODULE, '客户贴标申请进入合法待处理状态', created.response, - Boolean(relabelNo) && created.data?.application?.status === 'PENDING_PROCESSING' + Boolean(relabelNo) && created.data?.application?.status === 'PENDING_REVIEW' && created.data?.application?.customerOrderNo === order.row.orderNo, `relabelNo=${relabelNo || 'missing'}, status=${created.data?.application?.status || 'missing'}; ${trace(created.response)}`, OPTIONS) if (!relabelNo) return false @@ -145,9 +145,9 @@ async function verifyRelabel(context, token, order) { const detail = await merchant(context, token, 'GET', '客户查询自身贴标申请详情', `${RELABEL}/${encodeURIComponent(relabelNo)}`) context.assert(MODULE, '贴标详情保留明细与创建历史', detail.response, - id(detail.data?.application?.relabelNo) === relabelNo && detail.data?.application?.status === 'PENDING_PROCESSING' + id(detail.data?.application?.relabelNo) === relabelNo && detail.data?.application?.status === 'PENDING_REVIEW' && Array.isArray(detail.data?.items) && detail.data.items.length === 1 - && Array.isArray(detail.data?.history) && detail.data.history.some((item) => item?.toStatus === 'PENDING_PROCESSING'), + && Array.isArray(detail.data?.history) && detail.data.history.some((item) => item?.toStatus === 'PENDING_REVIEW'), `items=${detail.data?.items?.length ?? 'missing'}, histories=${detail.data?.history?.length ?? 'missing'}; ${trace(detail.response)}`, OPTIONS) context.createdResources.phase4OverseasClientServices.relabelNo = relabelNo return true @@ -299,7 +299,7 @@ export async function run(context) { const transferDone = await verifyTransfer(context, token, orders) const discardDone = await verifyDiscard(context, token, orders) context.createdResources.phase4OverseasClientServices.finalStatuses = { - relabel: relabelDone ? 'PENDING_PROCESSING' : null, + relabel: relabelDone ? 'PENDING_REVIEW' : null, transfer: transferDone ? 'APPROVED' : null, discard: discardDone ? 'PENDING_REVIEW' : null, } diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs index 71222f3d5..052e5d152 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs @@ -94,8 +94,9 @@ test('PDA operations reuse received plan pallets and the public location without /auxiliaryPallets: generatedContainerPallets\.filter/) assert.match(source, /resolvePublicLocation/) assert.match(source, /resolveReusableAuxiliaryPallet/) - assert.match(source, /locationType === 'VIRTUAL'/) - assert.match(source, /P3托盘已在公共库位不重复移库/) + assert.match(source, /\['STORAGE', 'NORMAL'\]\.includes\(item\?\.locationType\)/) + assert.match(source, /status === 'IDLE' && item\?\.locationCode !== sourceLocationCode/) + assert.doesNotMatch(source, /P3托盘已在公共库位不重复移库/) assert.doesNotMatch(source, /warehouse-admin[^'`]*\/locations`,\s*\{/) assert.doesNotMatch(source, /warehouse-admin[^'`]*\/pallets`,\s*\{/) assert.doesNotMatch(source, /createLocation|createEmptyPallet/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs index 41f2ab370..3b2505e99 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs @@ -98,11 +98,11 @@ async function resolvePallet(context, token, palletCode, name = '扫码解析托 '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode }) } -async function resolvePublicLocation(context, fixture) { +async function resolvePublicLocation(context, fixture, sourceLocationCode) { const response = await context.requestOverseas('GET', `/api/platform/v1/warehouse-admin/${encodeURIComponent(fixture.overseasWarehouseId)}/locations?page=1&size=200`) - const location = records(response?.body?.data).find((item) => item?.locationType === 'VIRTUAL' - && item?.status === 'ACTIVE') + const location = records(response?.body?.data).find((item) => ['STORAGE', 'NORMAL'].includes(item?.locationType) + && item?.status === 'IDLE' && item?.locationCode !== sourceLocationCode) context.addStep(MODULE, '复用当前海外仓公共库位', response, success(response) && longId(id(location?.id)) && nonBlank(location?.locationCode), `locationId=${id(location?.id) || 'missing'}, locationCode=${location?.locationCode || 'missing'}; ${trace(response)}`, OPTIONS) @@ -196,22 +196,14 @@ async function verifyStocktake(context, token, fixture) { } async function verifyInventoryMove(context, token, fixture) { - const target = await resolvePublicLocation(context, fixture) - if (!target) return false let pallet = (await resolvePallet(context, token, fixture.pallet.palletCode, 'P3移库前解析整托')).data if (!pallet || !nonBlank(pallet.locationCode)) { blocker(context, 'P3整托移库', '本轮完成托盘无法解析当前位置') return false } const originalLocationCode = pallet.locationCode - if (originalLocationCode === target.locationCode) { - const availablePackages = (pallet.inventory || []).reduce((sum, item) => - sum + Number(item?.availablePackages || 0), 0) - context.assert(MODULE, 'P3托盘已在公共库位不重复移库', null, - availablePackages > 0, - `palletCode=${pallet.palletCode}, locationCode=${target.locationCode}, availablePackages=${availablePackages}`, OPTIONS) - return availablePackages > 0 - } + const target = await resolvePublicLocation(context, fixture, originalLocationCode) + if (!target) return false const path = '/api/pda/v1/last-leg/inventory-moves' const body = { sourcePalletCode: pallet.palletCode, targetLocationCode: target.locationCode, expectedPalletVersion: Number(pallet.palletVersion), idempotencyKey: key(context, 'MOVE-FORWARD'), @@ -220,6 +212,7 @@ async function verifyInventoryMove(context, token, fixture) { context.assert(MODULE, 'P3整托移库数量和目标位置正确', moved.response, moved.data?.status === 'COMPLETED' && moved.data?.targetLocationCode === target.locationCode && Number(moved.data?.packageCount) > 0, trace(moved.response), OPTIONS) + if (!success(moved.response)) return false const replay = await request(context, token, 'POST', 'P3整托移库同键重放', path, body) context.assert(MODULE, 'P3移库重放标记且结果稳定', replay.response, replay.data?.replayed === true && replay.data?.moveNo === moved.data?.moveNo, trace(replay.response), OPTIONS) @@ -272,6 +265,7 @@ async function verifyPalletOperations(context, token, fixture) { target: { palletCode: auxiliary.palletCode, expectedVersion: Number(auxiliary.palletVersion) }, reason: `${context.marker}-swap` } const swapped = await replayOperation(context, token, 'P4整托换托', `${base}/swap`, swapBody, 'PALLET_SWAP') + if (!success(swapped.response)) return false await expectConflict(context, token, 'POST', 'P4换托同键异载荷A0409', `${base}/swap`, { ...swapBody, reason: `${swapBody.reason}-DIFFERENT` }) let occupiedSwap = operationPallet(swapped.data, auxiliary.palletCode) @@ -301,6 +295,7 @@ async function verifyPalletOperations(context, token, fixture) { reason: `${context.marker}-disassemble` } const disassembled = await replayOperation(context, token, 'P4拆托一箱到辅助托盘', `${base}/disassemble`, disassembleBody, 'PALLET_DISASSEMBLE') + if (!success(disassembled.response)) return false const splitOccupied = operationPallet(disassembled.data, auxiliary.palletCode) || (await resolvePallet(context, token, auxiliary.palletCode, 'P4拆托后解析辅助托盘')).data const mainAfterSplit = operationPallet(disassembled.data, main.palletCode) @@ -434,12 +429,16 @@ async function verifyRelabel(context, token, merchantToken, fixture) { const relabelNo = id(created?.application?.relabelNo) context.addStep(MODULE, 'P5基于本轮完成库存创建真实换标申请', createdResponse, success(createdResponse) && nonBlank(relabelNo) - && created?.application?.status === 'PENDING_PROCESSING', + && created?.application?.status === 'PENDING_REVIEW', `relabelNo=${relabelNo || 'missing'}, sku=${inventory.skuCode}; ${trace(createdResponse)}`, OPTIONS) if (!relabelNo) { blocker(context, 'P5真实换标申请', '平台正式换标创建接口未返回申请单号', createdResponse) return false } + const reviewed = await request(context, context.overseasToken, 'PUT', 'P5尾程订单审核直接贴标申请', + query(`/api/platform/v1/last-leg/orders/${encodeURIComponent(id(created.application.lastlegOrderId))}/relabel-review`, { customerId }), + { status: 'PENDING_PROCESSING', remark: context.marker }) + if (!success(reviewed.response) || reviewed.data?.application?.status !== 'PENDING_PROCESSING') return false const funded = await poll(async () => context.requestOverseas('GET', `${relabelBase}/${encodeURIComponent(relabelNo)}`), (response) => success(response) && response.body?.data?.application?.walletStatus === 'FROZEN') context.addStep(MODULE, 'P5换标费用冻结后开放PDA作业', funded.result, 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 266d84482..efb735e5e 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 @@ -244,6 +244,15 @@ async function relabelFlow(context, overseasToken, warehouseId, suffix, terminal `relabelNo=${relabelNo}, pollRequests=${frozen.attempts}, walletStatus=${frozen.result?.body?.data?.application?.walletStatus || 'missing'}`) if (frozen.result?.body?.data?.application?.walletStatus !== 'FROZEN') return null + const reviewStatus = terminalStatus === 'COMPLETED' ? 'PENDING_PROCESSING' : 'CANCELLED' + const review = await context.requestOverseas('PUT', query( + `/api/platform/v1/last-leg/orders/${encodeURIComponent(id(created.body.data.application.lastlegOrderId))}/relabel-review`, + { customerId: id(context.context.merchantQualificationFixture.tenantId) }), + { status: reviewStatus, remark: context.marker }) + assert(context, `${suffix}尾程订单审核直接贴标申请`, review, + success(review) && review.body.data?.application?.status === reviewStatus) + if (!success(review)) return null + if (terminalStatus === 'COMPLETED') { const processing = await context.requestOverseas('PUT', `${detailPath}/status`, { status: 'PROCESSING', remark: context.marker }) assert(context, '换标单冻结后才允许进入处理中', processing, @@ -257,7 +266,7 @@ async function relabelFlow(context, overseasToken, warehouseId, suffix, terminal assert(context, '换标客户确认后进入待出库', readyForOutbound, success(readyForOutbound) && readyForOutbound.body.data?.application?.status === 'READY_FOR_OUTBOUND') } - const terminal = await context.requestOverseas('PUT', `${detailPath}/status`, { status: terminalStatus, remark: context.marker }) + const terminal = terminalStatus === 'CANCELLED' ? review : await context.requestOverseas('PUT', `${detailPath}/status`, { status: terminalStatus, remark: context.marker }) assert(context, `${suffix}换标单进入${terminalStatus}`, terminal, success(terminal) && terminal.body.data?.application?.status === terminalStatus) const walletTerminal = terminalStatus === 'COMPLETED' ? 'SETTLED' : 'RELEASED' diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletTransitionService.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletTransitionService.java index f4cbe17c2..5c65171cf 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletTransitionService.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletTransitionService.java @@ -18,6 +18,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.util.HexFormat; +import java.util.Objects; import java.util.Set; @Service @@ -102,15 +103,15 @@ public class WarehousePalletTransitionService { WarehouseLocationEntity target; if (sourceLocationId <= targetLocationId) { source = requireLocation(warehouseId, sourceLocationId); - target = sourceLocationId == targetLocationId ? source : requireLocation(warehouseId, targetLocationId); + target = Objects.equals(sourceLocationId, targetLocationId) ? source : requireLocation(warehouseId, targetLocationId); } else { target = requireLocation(warehouseId, targetLocationId); source = requireLocation(warehouseId, sourceLocationId); } requireLocationMatchesPallet(source, pallet.getStatus()); - if (sourceLocationId != targetLocationId) requireAvailableTarget(target, targetStatus); + if (!Objects.equals(sourceLocationId, targetLocationId)) requireAvailableTarget(target, targetStatus); - if (sourceLocationId == targetLocationId) { + if (Objects.equals(sourceLocationId, targetLocationId)) { transitionSameLocation(warehouseId, source, pallet.getStatus(), targetStatus); } else { releaseSourceLocation(warehouseId, source, pallet.getStatus()); diff --git a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletTransitionServiceTest.java b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletTransitionServiceTest.java index 5d37cc44c..a63f0731e 100644 --- a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletTransitionServiceTest.java +++ b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletTransitionServiceTest.java @@ -48,6 +48,22 @@ class WarehousePalletTransitionServiceTest { assertEquals(4, captor.getValue().getBeforeVersion()); } + @Test void releasesOccupiedPalletAtSameLargeLocationId() { + long locationId = 2104611550538616833L; + when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-LARGE", "OCCUPIED", 4, locationId)); + when(mapper.lockPublicLocation(88L, locationId)).thenReturn(location(locationId, "STORAGE", "OCCUPIED")); + when(mapper.transitionLocation(88L, locationId, "OCCUPIED", "IDLE")).thenReturn(1); + when(mapper.transition(88L, 10L, "IDLE", locationId, 4)).thenReturn(1); + + var response = service.apply(10L, new PalletTransitionRequests.Apply( + 88L, 4, "IDLE", locationId, "large-location-release", "swap")); + + assertEquals("IDLE", response.afterStatus()); + assertEquals(locationId, response.afterLocationId()); + verify(mapper, times(1)).lockPublicLocation(88L, locationId); + verify(mapper).transitionLocation(88L, locationId, "OCCUPIED", "IDLE"); + } + @Test void exactReplayDoesNotMutatePhysicalPalletAgain() { WarehousePalletOperationEntity existing = operation(); when(mapper.findOperation("operation-1")).thenReturn(existing);