feat: enforce canonical container identity across regions
This commit is contained in:
+1
-1
@@ -106,7 +106,7 @@ class InboundUnloadCompletedOutboxRelayTest {
|
||||
}
|
||||
|
||||
private EventEnvelope event() {
|
||||
var payload = objectMapper.valueToTree(new OverseasContainerUnloadedV1("1001", "MSCU1234567",
|
||||
var payload = objectMapper.valueToTree(new OverseasContainerUnloadedV1("1001", "MSCU6639870",
|
||||
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null));
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000009",
|
||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1, Instant.parse("2026-08-20T03:00:01Z"),
|
||||
|
||||
+6
-6
@@ -45,7 +45,7 @@ class InboundUnloadCompletedOutboxServiceTest {
|
||||
Instant photoUploadedAt = Instant.parse("2026-08-24T08:00:00Z");
|
||||
Instant containerCompletedAt = Instant.parse("2026-08-24T09:30:00Z");
|
||||
|
||||
service.publishContainerCompleted("100", "booking-1", "MSCU1234567",
|
||||
service.publishContainerCompleted("100", "booking-1", "MSCU6639870",
|
||||
containerCompletedAt, photoUploadedAt, 7);
|
||||
|
||||
ArgumentCaptor<LastlegEventOutbox> captured = ArgumentCaptor.forClass(LastlegEventOutbox.class);
|
||||
@@ -64,7 +64,7 @@ class InboundUnloadCompletedOutboxServiceTest {
|
||||
void legacySinglePlanEntryUsesTaskCompletionTime() throws Exception {
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setSourceOrderId(11L);
|
||||
plan.setContainerNo("MSCU1234567");
|
||||
plan.setContainerNo("MSCU6639870");
|
||||
InboundPdaTask task = new InboundPdaTask();
|
||||
task.setCustomerId("100");
|
||||
task.setCompletedAt(LocalDateTime.parse("2026-08-24T09:30:00"));
|
||||
@@ -87,11 +87,11 @@ class InboundUnloadCompletedOutboxServiceTest {
|
||||
|
||||
@Test
|
||||
void repeatedContainerCompletionDoesNotPublishAnotherEvent() {
|
||||
LastlegEventOutbox existing = existing("100", "booking-1", "MSCU1234567");
|
||||
LastlegEventOutbox existing = existing("100", "booking-1", "MSCU6639870");
|
||||
when(mapper.findByEventKey(EventTypes.OVERSEAS_CONTAINER_UNLOADED + ":booking-1"))
|
||||
.thenReturn(existing);
|
||||
|
||||
service.publishContainerCompleted("100", "booking-1", "MSCU1234567",
|
||||
service.publishContainerCompleted("100", "booking-1", "MSCU6639870",
|
||||
Instant.parse("2026-08-24T10:00:00Z"), Instant.parse("2026-08-24T08:00:00Z"), 8);
|
||||
|
||||
verify(mapper, never()).insert(any(LastlegEventOutbox.class));
|
||||
@@ -99,11 +99,11 @@ class InboundUnloadCompletedOutboxServiceTest {
|
||||
|
||||
@Test
|
||||
void sameIdempotencyKeyCannotRepresentAnotherPhysicalContainer() {
|
||||
LastlegEventOutbox existing = existing("100", "booking-1", "MSCU1234567");
|
||||
LastlegEventOutbox existing = existing("100", "booking-1", "MSCU6639870");
|
||||
when(mapper.findByEventKey(EventTypes.OVERSEAS_CONTAINER_UNLOADED + ":booking-1"))
|
||||
.thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.publishContainerCompleted("100", "booking-1", "TGHU7654321",
|
||||
assertThatThrownBy(() -> service.publishContainerCompleted("100", "booking-1", "ZCSU7881970",
|
||||
Instant.parse("2026-08-24T10:00:00Z"), Instant.parse("2026-08-24T08:00:00Z"), 8))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("conflicts with another container");
|
||||
|
||||
+50
-17
@@ -98,12 +98,12 @@ class InboundPlanServiceTest {
|
||||
verify(orderMapper).insertHistory(any(), org.mockito.ArgumentMatchers.eq(2L), any(), any(), any());
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
assertEquals("CONT-1", result.containerNo());
|
||||
assertEquals("MSCU6639870", result.containerNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsAConsistentDestinationAssignedOrder() {
|
||||
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "CONT-1", "RK-2",
|
||||
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10,
|
||||
"2090000000000000001", "LAX-02", "Los Angeles 02", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(assigned));
|
||||
@@ -125,7 +125,7 @@ class InboundPlanServiceTest {
|
||||
existing.setId(9L);
|
||||
existing.setPlanNo("LIP9");
|
||||
existing.setCustomerId("100");
|
||||
existing.setContainerNo("CONT-1");
|
||||
existing.setContainerNo("MSCU6639870");
|
||||
existing.setStatus("PLANNED");
|
||||
existing.setLineCount(1);
|
||||
existing.setPalletCount(1);
|
||||
@@ -153,7 +153,7 @@ class InboundPlanServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
|
||||
@@ -183,7 +183,7 @@ class InboundPlanServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsTransferWarehouseThatDiffersFromCargoSnapshots() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
|
||||
@@ -196,8 +196,8 @@ class InboundPlanServiceTest {
|
||||
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
|
||||
.getAnnotation(Transactional.class));
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED")));
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B")));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
@@ -217,10 +217,10 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "CONT-1", "RK-2",
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
@@ -233,8 +233,8 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
|
||||
var refs = List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED"));
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED", "MSCU6639870"));
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs);
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2"));
|
||||
@@ -249,8 +249,8 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
@@ -259,13 +259,46 @@ class InboundPlanServiceTest {
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsContainerDriftAcrossCustomersBeforeWritingAnyPlan() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "CSQU3054383")));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReplayWhenExistingPlanContainerSnapshotHasDrifted() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_PLANNED", "MSCU6639870")));
|
||||
InboundPlan first = existingPlan("100", 1L, "LIP-1");
|
||||
InboundPlan drifted = existingPlan("200", 2L, "LIP-2");
|
||||
drifted.setContainerNo("CSQU3054383");
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(first);
|
||||
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(drifted);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
private InboundPlan existingPlan(String customerId, long orderId, String planNo) {
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(orderId + 100);
|
||||
plan.setPlanNo(planNo);
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(orderId);
|
||||
plan.setContainerNo("CONT-1");
|
||||
plan.setContainerNo("MSCU6639870");
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(1);
|
||||
plan.setPalletCount(1);
|
||||
@@ -274,12 +307,12 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, customerId, "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
return new SourceOrderSnapshot(id, customerId, "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
return new SourceOrderSnapshot(id, "100", "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user