fix: restore transfer creation replay before carton validation

This commit is contained in:
2026-10-23 02:59:48 +08:00
parent 85c7cde040
commit a7f680d69d
2 changed files with 47 additions and 20 deletions
@@ -214,6 +214,16 @@ public class WarehouseTransferService {
@Transactional
public WarehouseTransferResponses.Detail create(String customerId, String operatorId,
WarehouseTransferRequests.Create request) {
// 已成功创建的命令先恢复原业务身份;不能用后来冻结或换标后的箱状态否定重放。
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
if (replay != null) {
if (!requestHash.equals(replay.getRequestHash())) {
throw conflict("Idempotency key was reused with different warehouse transfer data");
}
return detail(customerId, replay.getTransferNo());
}
if (!request.isInboundModeValid()) throw badRequest("外来货物须填写入仓信息,库存转运须选择FBA仓或私人仓");
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
if (itemPackages != request.packageCount()) {
@@ -224,16 +234,6 @@ public class WarehouseTransferService {
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
request.privateDestinationContactName(), request.privateDestinationContactPhone());
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
if (replay != null) {
if (!requestHash.equals(replay.getRequestHash())) {
throw conflict("Idempotency key was reused with different warehouse transfer data");
}
return detail(customerId, replay.getTransferNo());
}
var customer = customerDirectory.find(customerId);
if (customer == null || !customerId.equals(customer.id()) || !StringUtils.hasText(customer.name())) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "客户名称暂不可用,请稍后重试");
@@ -75,6 +75,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doAnswer;
@@ -85,6 +86,24 @@ import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class WarehouseTransferServiceTest {
@Test
void creationReplayRestoresOriginalTransferBeforeRecheckingChangedCartonAvailability() {
var request = relabelCartonRequest("BOX-ORIGINAL");
var existing = persistedTransfer();
existing.setRequestHash(org.springframework.test.util.ReflectionTestUtils.invokeMethod(service,"hash",request));
when(transfers.findKey("TENANT-A",request.idempotencyKey())).thenReturn(existing);
when(transfers.findNo("TENANT-A",existing.getTransferNo())).thenReturn(existing);
assertEquals(existing.getTransferNo(),service.create("TENANT-A","USER-1",request).transferNo());
verifyNoInteractions(inboundSnapshots,customerDirectory,inboundOrders);
verify(orders,never()).insert(any(LastlegOrder.class));
verify(orders,never()).updateById(any(LastlegOrder.class));
verify(transfers,never()).insert(any(WarehouseTransferOrder.class));
clearInvocations(orders);
existing.setRequestHash("different-original-payload");
var conflict=assertThrows(ResponseStatusException.class,()->service.create("TENANT-A","USER-1",request));
assertEquals(409,conflict.getStatusCode().value());
verifyNoInteractions(inboundSnapshots,customerDirectory,inboundOrders,orders);
}
@Test
void detailIncludesOnlyTheCoordinatorScopedDurableReceiptProjection() {
var transfer=transfer(WarehouseTransferService.PARTIALLY_RECEIVED);
@@ -488,18 +507,13 @@ class WarehouseTransferServiceTest {
assertEquals(400, exception.getStatusCode().value());
assertEquals("Warehouse transfer relabel only supports one destination warehouse per order", exception.getReason());
verifyNoInteractions(transfers);
verify(transfers).findKey("TENANT-A",request.idempotencyKey());
verifyNoMoreInteractions(transfers);
}
@Test
void transferRelabelRejectsUnknownAndForeignOrderCartonsBeforePersistence() {
var request = new WarehouseTransferRequests.Create(
"IN-1", List.of(11L), "WH-1", null, "FBA", null, "ONT8", null,
null, null, null, null, null, null, null, 1,
BigDecimal.ZERO, null, null, null, null, null, null, null, true,
LocalDateTime.of(2026, 10, 16, 10, 0), "Contact", "123", null, "TRUCK",
"carton-ownership", List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 1, null)),
List.of(new WarehouseTransferRequests.RelabelItem("CARTON_LABEL", List.of("FOREIGN-BOX"), "SKU-1", null, 1, "FBA-1", "ONT8", null)));
var request = relabelCartonRequest("FOREIGN-BOX");
for (var cartons : List.of(List.<InboundSnapshotMapper.SourceCarton>of(),
List.of(new InboundSnapshotMapper.SourceCarton("CURRENT-BOX", "SKU-1", null, null, null, null, null)))) {
when(inboundSnapshots.sourceCartons("TENANT-A", "WH-1", "IN-1")).thenReturn(cartons);
@@ -510,7 +524,8 @@ class WarehouseTransferServiceTest {
.thenReturn(List.of(new InboundSnapshotMapper.SourceCarton("FOREIGN-BOX", "SKU-1", null, null, null, null, null)));
org.junit.jupiter.api.Assertions.assertDoesNotThrow(() ->
ReflectionTestUtils.invokeMethod(service, "validateRelabelRequest", "TENANT-A", request));
verifyNoInteractions(transfers);
verify(transfers,times(2)).findKey("TENANT-A",request.idempotencyKey());
verifyNoMoreInteractions(transfers);
}
/** 先按原订单来源快照拒绝外部换标箱,不进入后续选箱占用。 */
@@ -546,7 +561,9 @@ class WarehouseTransferServiceTest {
var error = assertThrows(ResponseStatusException.class, () -> service.create("TENANT-A","USER-1",request));
assertEquals(400,error.getStatusCode().value());
verifyNoInteractions(transfers,orders);
verify(transfers).findKey("TENANT-A",request.idempotencyKey());
verifyNoMoreInteractions(transfers);
verifyNoInteractions(orders);
}
@Test
@@ -1279,6 +1296,16 @@ class WarehouseTransferServiceTest {
verify(transfers, never()).insert(any(WarehouseTransferOrder.class));
}
private static WarehouseTransferRequests.Create relabelCartonRequest(String cartonMark) {
return new WarehouseTransferRequests.Create(
"IN-1", List.of(11L), "WH-1", null, "FBA", null, "ONT8", null,
null, null, null, null, null, null, null, 1,
BigDecimal.ZERO, null, null, null, null, null, null, null, true,
LocalDateTime.of(2026, 10, 16, 10, 0), "Contact", "123", null, "TRUCK",
"carton-ownership", List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 1, null)),
List.of(new WarehouseTransferRequests.RelabelItem("CARTON_LABEL", List.of(cartonMark), "SKU-1", null, 1, "FBA-1", "ONT8", null)));
}
private static WarehouseTransferRequests.Create request(boolean needRelabel) {
if (!needRelabel) {
return new WarehouseTransferRequests.Create(