feat: gate overseas receiving and harden phase4 workflows
This commit is contained in:
+5
-1
@@ -18,7 +18,11 @@ class UsTruckOrderSchemaContractTest {
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_pallet"));
|
||||
assertTrue(sql.contains("pallet_count INT NOT NULL DEFAULT 1"));
|
||||
assertTrue(sql.contains("received_pallet_count INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("receipt_idempotency_key VARCHAR(255) NULL"));
|
||||
assertTrue(sql.contains("allocated_pallet_count INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_operation"));
|
||||
assertTrue(sql.contains("UNIQUE KEY uk_ll_us_truck_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
assertTrue(sql.contains("operation_type VARCHAR(64) NOT NULL"));
|
||||
assertTrue(sql.contains("idempotency_key VARCHAR(255) NULL COMMENT '操作幂等键'"));
|
||||
assertTrue(sql.contains("UNIQUE KEY uk_ll_us_truck_order_key(customer_id,idempotency_key)"));
|
||||
assertTrue(sql.contains("KEY idx_ll_us_truck_history(customer_id,order_id,created_at)"));
|
||||
}
|
||||
|
||||
+25
-2
@@ -37,7 +37,7 @@ class UsTruckOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void platformStatusChangeRequiresManagePermission() {
|
||||
var request = new UsTruckOrderRequests.ChangeStatus(0L, "DISPATCHED", null);
|
||||
var request = new UsTruckOrderRequests.ChangeStatus(0L, "CANCELLED", "cancel-1", null);
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> platform.changeStatus(user(0L, "PLATFORM", Set.of()), "100", "TK1", request));
|
||||
assertEquals(403, error.getStatusCode().value());
|
||||
@@ -45,11 +45,34 @@ class UsTruckOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void platformStatusChangeUsesExplicitTargetTenant() {
|
||||
var request = new UsTruckOrderRequests.ChangeStatus(0L, "DISPATCHED", "truck assigned");
|
||||
var request = new UsTruckOrderRequests.ChangeStatus(0L, "CANCELLED", "cancel-1", "cancelled");
|
||||
platform.changeStatus(user(0L, "PLATFORM", Set.of(UsTruckOrderService.MANAGE_PERMISSION)), "100", "TK1", request);
|
||||
verify(service).changeStatus("100", "TK1", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformLifecycleCommandsUseExplicitTargetAndStatusPermission() {
|
||||
var user = user(0L, "PLATFORM", Set.of(UsTruckOrderService.MANAGE_PERMISSION));
|
||||
var review = new UsTruckOrderRequests.Review(0L, "APPROVE", "review-1", null);
|
||||
var overage = new UsTruckOrderRequests.ReviewPalletOverage(2L, "APPROVE", "overage-1", null);
|
||||
var allocation = new UsTruckOrderRequests.AllocatePallets(3L, java.util.List.of(1L), "allocate-1", null);
|
||||
var command = new UsTruckOrderRequests.Command(4L, "command-1", null);
|
||||
platform.review(user, "100", "TK1", review);
|
||||
platform.reviewPalletOverage(user, "100", "TK1", overage);
|
||||
platform.allocatePallets(user, "100", "TK1", allocation);
|
||||
platform.prepareDispatch(user, "100", "TK1", command);
|
||||
platform.dispatch(user, "100", "TK1", command);
|
||||
platform.startTransit(user, "100", "TK1", command);
|
||||
platform.complete(user, "100", "TK1", command);
|
||||
verify(service).review("100", "TK1", "7", review);
|
||||
verify(service).reviewPalletOverage("100", "TK1", "7", overage);
|
||||
verify(service).allocatePallets("100", "TK1", "7", allocation);
|
||||
verify(service).prepareDispatch("100", "TK1", "7", command);
|
||||
verify(service).dispatch("100", "TK1", "7", command);
|
||||
verify(service).startTransit("100", "TK1", "7", command);
|
||||
verify(service).complete("100", "TK1", "7", command);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformCreateUsesExplicitCustomerAndCreatePermission() {
|
||||
var request = new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null,
|
||||
|
||||
+128
-106
@@ -2,9 +2,11 @@ package com.baiyue.lastleg.trucking.service;
|
||||
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderOperation;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderPallet;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderStatusHistory;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderOperationMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderPalletMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderStatusHistoryMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -16,18 +18,18 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -35,7 +37,12 @@ class UsTruckOrderServiceTest {
|
||||
@Mock UsTruckOrderMapper orders;
|
||||
@Mock UsTruckOrderPalletMapper pallets;
|
||||
@Mock UsTruckOrderStatusHistoryMapper histories;
|
||||
@Mock UsTruckOrderOperationMapper operations;
|
||||
private final List<UsTruckOrderPallet> palletRows = new ArrayList<>();
|
||||
private final List<UsTruckOrderStatusHistory> historyRows = new ArrayList<>();
|
||||
private final Map<String, UsTruckOrderOperation> operationRows = new HashMap<>();
|
||||
private UsTruckOrderService service;
|
||||
private UsTruckOrder order;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@@ -43,113 +50,128 @@ class UsTruckOrderServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "pallets", pallets);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
org.mockito.Mockito.lenient().when(pallets.listOrder(anyString(), any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerOrderStartsPendingConfirmationAndUsesUsdByDefault() {
|
||||
AtomicReference<UsTruckOrder> saved = new AtomicReference<>();
|
||||
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> { saved.set(call.getArgument(0)); return 1; });
|
||||
when(orders.findNo(anyString(), anyString())).thenAnswer(call -> saved.get());
|
||||
when(histories.listOrder(anyString(), any())).thenReturn(List.of());
|
||||
|
||||
var detail = service.create("100", "7", create("key-1"));
|
||||
|
||||
assertEquals(UsTruckOrderService.PENDING_CONFIRMATION, detail.summary().status());
|
||||
assertEquals("USD", detail.summary().currency());
|
||||
assertEquals("100", detail.summary().customerId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusChangePersistsHistoryAndIncrementsVersion() {
|
||||
UsTruckOrder order = order(UsTruckOrderService.PENDING_CONFIRMATION, 0L);
|
||||
when(orders.lockNo("100", "TK1")).thenReturn(order);
|
||||
when(orders.findNo("100", "TK1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
|
||||
var result = service.changeStatus("100", "TK1", "7",
|
||||
new UsTruckOrderRequests.ChangeStatus(0L, "WAITING_INBOUND", "accepted"));
|
||||
|
||||
assertEquals(UsTruckOrderService.WAITING_INBOUND, result.summary().status());
|
||||
assertEquals(1, result.summary().version());
|
||||
verify(histories).insert(any(UsTruckOrderStatusHistory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleStatusChangeIsRejected() {
|
||||
when(orders.lockNo("100", "TK1")).thenReturn(order(UsTruckOrderService.DISPATCHED, 2L));
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
|
||||
service.changeStatus("100", "TK1", "7", new UsTruckOrderRequests.ChangeStatus(1L, "IN_TRANSIT", null)));
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleOrdinaryUpdateIsRejectedAsConflict() {
|
||||
when(orders.lockNo("100", "TK1")).thenReturn(order(UsTruckOrderService.PENDING_REVIEW, 2L));
|
||||
UsTruckOrderRequests.Update request = new UsTruckOrderRequests.Update(1L,
|
||||
null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.update("100", "TK1", "7", request));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailIncludesBoundPallets() {
|
||||
UsTruckOrder order = order(UsTruckOrderService.WAITING_INBOUND, 1L);
|
||||
UsTruckOrderPallet pallet = new UsTruckOrderPallet();
|
||||
pallet.setId(10L); pallet.setPalletNo("PLT-001"); pallet.setSequenceNo(1);
|
||||
pallet.setSource("SYSTEM"); pallet.setStatus("OCCUPIED");
|
||||
pallet.setWarehousePalletId(20L); pallet.setPackageCount(12);
|
||||
when(orders.findNo("100", "TK1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
when(pallets.listOrder("100", 1L)).thenReturn(List.of(pallet));
|
||||
|
||||
var detail = service.detail("100", "TK1");
|
||||
|
||||
assertEquals(1, detail.pallets().size());
|
||||
assertEquals("PLT-001", detail.pallets().get(0).palletNo());
|
||||
assertEquals(12, detail.pallets().get(0).packageCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingMorePalletsCreatesOverageAndKeepsDeclaredCount() {
|
||||
UsTruckOrder order = order(UsTruckOrderService.WAITING_INBOUND, 0L);
|
||||
order.setPalletCount(1);
|
||||
when(orders.lockNo("100", "TK1")).thenReturn(order);
|
||||
when(orders.findNo("100", "TK1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
List<UsTruckOrderPallet> savedPallets = new ArrayList<>();
|
||||
when(pallets.listOrder("100", 1L)).thenAnswer(call -> List.copyOf(savedPallets));
|
||||
when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
|
||||
savedPallets.add(call.getArgument(0));
|
||||
return 1;
|
||||
order = order(UsTruckOrderService.PENDING_REVIEW, 0L, 2);
|
||||
when(orders.lockNo("100", "TK1")).thenAnswer(call -> order);
|
||||
when(orders.findNo("100", "TK1")).thenAnswer(call -> order);
|
||||
when(pallets.listOrder("100", 1L)).thenAnswer(call -> palletRows);
|
||||
when(histories.listOrder("100", 1L)).thenAnswer(call -> historyRows);
|
||||
when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
|
||||
historyRows.add(call.getArgument(0)); return 1;
|
||||
});
|
||||
|
||||
var result = service.receivePallets("100", "TK1", "7",
|
||||
new UsTruckOrderRequests.ReceivePallets(0L, 3, 0, "receive-1", "actual three pallets"));
|
||||
|
||||
assertEquals(1, result.summary().palletCount());
|
||||
assertEquals(3, result.summary().receivedPalletCount());
|
||||
assertEquals(UsTruckOrderService.PALLET_OVERAGE_REVIEW, result.summary().status());
|
||||
verify(pallets, org.mockito.Mockito.times(3)).insert(any(UsTruckOrderPallet.class));
|
||||
when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
|
||||
UsTruckOrderPallet row = call.getArgument(0); palletRows.add(row); return 1;
|
||||
});
|
||||
when(operations.findKey(anyString(), anyLong(), anyString()))
|
||||
.thenAnswer(call -> operationRows.get(call.getArgument(2)));
|
||||
when(operations.insert(any(UsTruckOrderOperation.class))).thenAnswer(call -> {
|
||||
UsTruckOrderOperation row = call.getArgument(0); operationRows.put(row.getIdempotencyKey(), row); return 1;
|
||||
});
|
||||
UsTruckOrderStatusHistory created = new UsTruckOrderStatusHistory();
|
||||
created.setOperationType("CREATE"); created.setOperatorId("creator"); created.setIdempotencyKey("create-1");
|
||||
created.setToStatus(UsTruckOrderService.PENDING_REVIEW); historyRows.add(created);
|
||||
}
|
||||
|
||||
private UsTruckOrderRequests.Create create(String key) {
|
||||
return new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles Port", "Pier 1", "Ontario, CA",
|
||||
"Warehouse A", "40HQ", 100, new BigDecimal("12600"), new BigDecimal("55.2"),
|
||||
new BigDecimal("3800"), null, "Alice", "+1-555-0100", LocalDate.now().plusDays(2), null, key);
|
||||
@Test
|
||||
void completeLifecycleUsesFactsIdempotencyOperatorsAndCannotReverseTerminal() {
|
||||
var review = new UsTruckOrderRequests.Review(0L, "APPROVE", "review-1", "approve");
|
||||
var reviewed = service.review("100", "TK1", "operator-7", review);
|
||||
assertEquals(UsTruckOrderService.WAITING_INBOUND, reviewed.summary().status());
|
||||
assertEquals(2, palletRows.size());
|
||||
assertEquals(1L, reviewed.summary().version());
|
||||
assertEquals(1L, service.review("100", "TK1", "operator-7", review).summary().version());
|
||||
assertThrows(ResponseStatusException.class, () -> service.review("100", "TK1", "operator-7",
|
||||
new UsTruckOrderRequests.Review(1L, "REJECT", "review-1", "different")));
|
||||
|
||||
var receive = new UsTruckOrderRequests.ReceivePallets(1L, 3, 6, List.of(2, 2, 2),
|
||||
"receive-1", "receive overage");
|
||||
var received = service.receivePallets("100", "TK1", "receiver-8", receive);
|
||||
assertEquals(UsTruckOrderService.PALLET_OVERAGE_REVIEW, received.summary().status());
|
||||
assertEquals(3, received.summary().receivedPalletCount());
|
||||
assertEquals(3, palletRows.size());
|
||||
assertEquals(2L, service.receivePallets("100", "TK1", "receiver-8", receive).summary().version());
|
||||
|
||||
var overage = new UsTruckOrderRequests.ReviewPalletOverage(2L, "APPROVE",
|
||||
"overage-1", "approve overage");
|
||||
var overageReviewed = service.reviewPalletOverage("100", "TK1", "reviewer-9", overage);
|
||||
assertEquals(UsTruckOrderService.WAITING_CONSOLIDATION, overageReviewed.summary().status());
|
||||
assertEquals(3, overageReviewed.summary().palletCount());
|
||||
assertTrue(palletRows.stream().allMatch(row -> "RECEIVED".equals(row.getStatus())));
|
||||
|
||||
Long first = palletRows.get(0).getId();
|
||||
List<Long> remaining = palletRows.subList(1, 3).stream().map(UsTruckOrderPallet::getId).toList();
|
||||
var partial = service.allocatePallets("100", "TK1", "allocator-10",
|
||||
new UsTruckOrderRequests.AllocatePallets(3L, List.of(first), "allocate-1", "partial"));
|
||||
assertEquals(UsTruckOrderService.PARTIALLY_ALLOCATED, partial.summary().status());
|
||||
assertEquals(1, partial.summary().allocatedPalletCount());
|
||||
var allocated = service.allocatePallets("100", "TK1", "allocator-10",
|
||||
new UsTruckOrderRequests.AllocatePallets(4L, remaining, "allocate-2", "all"));
|
||||
assertEquals(UsTruckOrderService.ALLOCATED, allocated.summary().status());
|
||||
assertEquals(3, allocated.summary().allocatedPalletCount());
|
||||
|
||||
assertEquals(UsTruckOrderService.PENDING_DISPATCH, service.prepareDispatch("100", "TK1", "dispatcher-11",
|
||||
command(5L, "prepare-1")).summary().status());
|
||||
assertEquals(UsTruckOrderService.DISPATCHED, service.dispatch("100", "TK1", "dispatcher-11",
|
||||
command(6L, "dispatch-1")).summary().status());
|
||||
assertEquals(UsTruckOrderService.IN_TRANSIT, service.startTransit("100", "TK1", "driver-12",
|
||||
command(7L, "transit-1")).summary().status());
|
||||
var completed = service.complete("100", "TK1", "driver-12", command(8L, "complete-1"));
|
||||
assertEquals(UsTruckOrderService.COMPLETED, completed.summary().status());
|
||||
assertEquals(9L, completed.summary().version());
|
||||
assertEquals(9L, service.complete("100", "TK1", "driver-12", command(8L, "complete-1"))
|
||||
.summary().version());
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.changeStatus("100", "TK1", "operator-7",
|
||||
new UsTruckOrderRequests.ChangeStatus(9L, "CANCELLED", "cancel-terminal", "forbidden")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.startTransit("100", "TK1", "driver-12",
|
||||
command(8L, "reverse-state")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.dispatch("100", "TK1", "dispatcher-11",
|
||||
command(1L, "stale-version")));
|
||||
|
||||
assertEquals(List.of("CREATE", "REVIEW", "RECEIVE_PALLETS", "REVIEW_PALLET_OVERAGE",
|
||||
"ALLOCATE_PALLETS", "ALLOCATE_PALLETS", "PREPARE_DISPATCH", "DISPATCH",
|
||||
"START_TRANSIT", "COMPLETE"),
|
||||
historyRows.stream().map(UsTruckOrderStatusHistory::getOperationType).toList());
|
||||
assertTrue(historyRows.stream().skip(1).allMatch(row -> row.getOperatorId() != null
|
||||
&& row.getIdempotencyKey() != null));
|
||||
}
|
||||
|
||||
private UsTruckOrder order(String status, long version) {
|
||||
UsTruckOrder order = new UsTruckOrder(); order.setId(1L); order.setCustomerId("100"); order.setOrderNo("TK1");
|
||||
order.setTransportType("PORT_PICKUP"); order.setOrigin("Los Angeles"); order.setDestination("Ontario");
|
||||
order.setContainerType("40HQ"); order.setPalletCount(1); order.setPackageCount(0);
|
||||
order.setReceivedPalletCount(0); order.setAllocatedPalletCount(0); order.setShippedPalletCount(0);
|
||||
order.setStatus(status); order.setVersion(version);
|
||||
order.setCreatedAt(LocalDateTime.now()); order.setUpdatedAt(order.getCreatedAt()); return order;
|
||||
@Test
|
||||
void partialReceiptUsesReceivingThenExactReceiptMovesToConsolidation() {
|
||||
service.review("100", "TK1", "operator", new UsTruckOrderRequests.Review(
|
||||
0L, "APPROVE", "review-partial", null));
|
||||
var partial = service.receivePallets("100", "TK1", "receiver",
|
||||
new UsTruckOrderRequests.ReceivePallets(1L, 1, 2, List.of(2), "receive-partial", null));
|
||||
assertEquals(UsTruckOrderService.RECEIVING, partial.summary().status());
|
||||
var exact = service.receivePallets("100", "TK1", "receiver",
|
||||
new UsTruckOrderRequests.ReceivePallets(2L, 2, 4, List.of(2, 2), "receive-exact", null));
|
||||
assertEquals(UsTruckOrderService.WAITING_CONSOLIDATION, exact.summary().status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectedOverageExcludesExtraPalletFromAcceptedCount() {
|
||||
service.review("100", "TK1", "operator", new UsTruckOrderRequests.Review(
|
||||
0L, "APPROVE", "review-reject-overage", null));
|
||||
service.receivePallets("100", "TK1", "receiver", new UsTruckOrderRequests.ReceivePallets(
|
||||
1L, 3, 6, List.of(2, 2, 2), "receive-reject-overage", null));
|
||||
var reviewed = service.reviewPalletOverage("100", "TK1", "reviewer",
|
||||
new UsTruckOrderRequests.ReviewPalletOverage(2L, "REJECT", "reject-overage", null));
|
||||
assertEquals(2, reviewed.summary().receivedPalletCount());
|
||||
assertEquals("OVERAGE_REJECTED", palletRows.get(2).getStatus());
|
||||
}
|
||||
|
||||
private UsTruckOrderRequests.Command command(long version, String key) {
|
||||
return new UsTruckOrderRequests.Command(version, key, key);
|
||||
}
|
||||
|
||||
private UsTruckOrder order(String status, long version, int pallets) {
|
||||
UsTruckOrder value = new UsTruckOrder();
|
||||
value.setId(1L); value.setCustomerId("100"); value.setOrderNo("TK1");
|
||||
value.setTransportType("PORT_PICKUP"); value.setOrigin("Los Angeles"); value.setDestination("Ontario");
|
||||
value.setPalletCount(pallets); value.setPackageCount(0); value.setReceivedPalletCount(0);
|
||||
value.setAllocatedPalletCount(0); value.setStatus(status); value.setVersion(version);
|
||||
value.setCreatedAt(LocalDateTime.now()); value.setUpdatedAt(value.getCreatedAt());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user