feat: gate overseas receiving and harden phase4 workflows
This commit is contained in:
+2
@@ -8,6 +8,7 @@ import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -52,6 +53,7 @@ class LastlegOrderControllerTest {
|
||||
private LastlegOrderController controller() {
|
||||
LastlegOrderController controller = new LastlegOrderController();
|
||||
ReflectionTestUtils.setField(controller, "consumer", mock(LoadingCompletedConsumer.class));
|
||||
ReflectionTestUtils.setField(controller, "shippingNodeConsumer", mock(ContainerShippingNodeConsumer.class));
|
||||
ReflectionTestUtils.setField(controller, "orderService", service);
|
||||
ReflectionTestUtils.setField(controller, "tokenGuard", mock(InternalServiceTokenGuard.class));
|
||||
return controller;
|
||||
|
||||
+5
-4
@@ -51,7 +51,8 @@ class ReceivingContainerContractTest {
|
||||
void everyBrowserReachableReceivingMethodUsesOnlyPdaOperatePermission() {
|
||||
PreAuthorize permission = ReceivingContainerController.class.getAnnotation(PreAuthorize.class);
|
||||
assertTrue(permission != null);
|
||||
assertTrue("hasAuthority('lastleg:pda:operate')".equals(permission.value()));
|
||||
assertTrue("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')"
|
||||
.equals(permission.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,9 +132,9 @@ class ReceivingContainerContractTest {
|
||||
String receivingPaths = yaml.substring(yaml.indexOf(prefix), yaml.indexOf("/api/platform/v1/last-leg/orders:"));
|
||||
List<String> successResponses = receivingPaths.lines().map(String::trim)
|
||||
.filter(line -> line.startsWith("responses: { '200':")).toList();
|
||||
assertEquals(12, successResponses.size());
|
||||
assertTrue(successResponses.stream().allMatch(line ->
|
||||
line.contains("content: { application/json: { schema: { $ref: '#/components/schemas/Receiving")));
|
||||
assertEquals(16, successResponses.size());
|
||||
assertEquals(12, successResponses.stream().filter(line -> line.contains("/schemas/Receiving")).count());
|
||||
assertEquals(4, successResponses.stream().filter(line -> line.contains("/schemas/Pda")).count());
|
||||
assertTrue(receivingPaths.contains("返回 WAIT_SIGN、SIGNED 或 RECEIVING 当前柜"));
|
||||
assertTrue(receivingPaths.contains("仅 PLATFORM WAREHOUSE_PICKER 可用"));
|
||||
assertFalse(receivingPaths.contains("客户与操作账号均从 PDA Token 获取"));
|
||||
|
||||
+4
-2
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.pda.controller;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerResponses;
|
||||
import com.baiyue.lastleg.inboundplan.pda.service.ReceivingContainerService;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -57,13 +58,14 @@ class ReceivingContainerControllerTest {
|
||||
@Test
|
||||
void pickerAuthorityWithoutWarehouseBindingIsForbidden() {
|
||||
AuthenticatedUser unbound = new AuthenticatedUser(42L, 0L, 0L, 8L, "receiver", "PLATFORM", "PDA",
|
||||
Set.of(ReceivingContainerController.OPERATE_PERMISSION));
|
||||
Set.of(PdaWarehouseIdentity.PICKER_ROLE, ReceivingContainerController.OPERATE_PERMISSION));
|
||||
assertThatThrownBy(() -> controller.candidates(unbound)).isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("403");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser picker() {
|
||||
return new AuthenticatedUser(42L, 0L, 0L, 8L, "receiver", "PLATFORM", "PDA",
|
||||
Set.of(ReceivingContainerController.OPERATE_PERMISSION), false, null, null, false, 900L);
|
||||
Set.of(PdaWarehouseIdentity.PICKER_ROLE, ReceivingContainerController.OPERATE_PERMISSION),
|
||||
false, null, null, false, 900L);
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -23,6 +23,7 @@ import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerSessionMapper
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -57,6 +58,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
@Mock ReceivingContainerSessionMapper sessions;
|
||||
@Mock ReceivingContainerOperationMapper operations;
|
||||
@Mock ReceivingSkuReceiptMapper skuReceipts;
|
||||
@Mock ContainerShippingGateMapper shippingGates;
|
||||
@Mock WarehouseResourceGateway warehouse;
|
||||
@Mock UnloadPhotoFileGateway unloadPhotos;
|
||||
@Mock InboundPdaWorkflowService workflow;
|
||||
@@ -75,11 +77,25 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
ReflectionTestUtils.setField(service, "sessions", sessions);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "skuReceipts", skuReceipts);
|
||||
ReflectionTestUtils.setField(service, "shippingGates", shippingGates);
|
||||
ReflectionTestUtils.setField(service, "warehouse", warehouse);
|
||||
ReflectionTestUtils.setField(service, "unloadPhotos", unloadPhotos);
|
||||
ReflectionTestUtils.setField(service, "workflow", workflow);
|
||||
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
service.initializeObjectMapper();
|
||||
org.mockito.Mockito.lenient().when(shippingGates.receivingAvailable(any(), any())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerIsHiddenAndPrepareRejectedBeforePickupReady() {
|
||||
when(plans.selectList(any())).thenReturn(List.of(plan("CONT-A")));
|
||||
when(shippingGates.receivingAvailable("100", "CONT-A")).thenReturn(0);
|
||||
|
||||
assertThat(service.candidates("7", 100L)).isEmpty();
|
||||
assertThatThrownBy(() -> service.prepare("7", "receiver", 100L, "CONT-A",
|
||||
new ReceivingContainerRequests.Action("not-ready")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
verifyNoInteractions(warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -315,6 +331,16 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
verifyNoInteractions(unloadPhotos);
|
||||
}
|
||||
|
||||
@Test
|
||||
void signRejectsMissingPhotosBeforeFileValidation() {
|
||||
when(sessions.lockActiveByOperator("OPERATOR:7")).thenReturn(session("CONT-A", "WAIT_SIGN"));
|
||||
ReceivingContainerRequests.Sign request = new ReceivingContainerRequests.Sign(List.of(), "missing-photos");
|
||||
|
||||
assertThatThrownBy(() -> service.sign("7", "receiver", request))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
verifyNoInteractions(unloadPhotos);
|
||||
}
|
||||
|
||||
@Test
|
||||
void signRejectsDuplicateFileIdsBeforeFileValidation() {
|
||||
when(sessions.lockActiveByOperator("OPERATOR:7")).thenReturn(session("CONT-A", "WAIT_SIGN"));
|
||||
|
||||
+2
@@ -15,6 +15,8 @@ class LocalLooseboardSchemaContractTest {
|
||||
}
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_local_looseboard_order"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_local_looseboard_history"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_local_looseboard_operation"));
|
||||
assertTrue(sql.contains("uk_ll_local_looseboard_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
|
||||
assertTrue(sql.contains("uk_ll_local_looseboard_no(customer_id,order_no)"));
|
||||
assertTrue(sql.contains("lastleg_order_id BIGINT NULL"));
|
||||
|
||||
+59
-2
@@ -3,8 +3,10 @@ package com.baiyue.lastleg.looseboard.service;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
|
||||
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
|
||||
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
|
||||
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOperation;
|
||||
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardHistoryMapper;
|
||||
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOrderMapper;
|
||||
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOperationMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
@@ -22,6 +24,8 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -29,27 +33,38 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LocalLooseboardServiceTest {
|
||||
@Mock LocalLooseboardOrderMapper orders;
|
||||
@Mock LocalLooseboardHistoryMapper histories;
|
||||
@Mock LocalLooseboardOperationMapper operations;
|
||||
@Mock LastlegOrderMapper lastlegOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper lastlegHistories;
|
||||
@Mock LastlegOrderNoGenerator lastlegOrderNoGenerator;
|
||||
private LocalLooseboardService service;
|
||||
private final Map<String, LocalLooseboardOperation> operationRows = new HashMap<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new LocalLooseboardService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "lastlegOrders", lastlegOrders);
|
||||
ReflectionTestUtils.setField(service, "lastlegHistories", lastlegHistories);
|
||||
ReflectionTestUtils.setField(service, "lastlegOrderNoGenerator", lastlegOrderNoGenerator);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
org.mockito.Mockito.lenient().when(lastlegOrderNoGenerator.next()).thenReturn("LMO-LOOSE-1");
|
||||
org.mockito.Mockito.lenient().when(operations.findKey(anyString(), any(), anyString()))
|
||||
.thenAnswer(call -> operationRows.get(call.getArgument(2)));
|
||||
org.mockito.Mockito.lenient().when(operations.insert(any(LocalLooseboardOperation.class))).thenAnswer(call -> {
|
||||
LocalLooseboardOperation value = call.getArgument(0);
|
||||
operationRows.put(value.getIdempotencyKey(), value);
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,12 +118,54 @@ class LocalLooseboardServiceTest {
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
|
||||
var detail = service.changeStatus("100", "LB1", "7",
|
||||
new LocalLooseboardRequests.ChangeStatus(0L, "DISPATCHED", "accepted"));
|
||||
new LocalLooseboardRequests.ChangeStatus(0L, "DISPATCHED", "status-1", "accepted"));
|
||||
|
||||
assertEquals(LocalLooseboardService.DISPATCHED, detail.summary().status());
|
||||
assertEquals(1L, detail.summary().version());
|
||||
verify(histories).insert(org.mockito.ArgumentMatchers.<LocalLooseboardHistory>argThat(value ->
|
||||
"STATUS".equals(value.getOperationType()) && "DISPATCHED".equals(value.getToStatus())));
|
||||
"STATUS".equals(value.getOperationType()) && "DISPATCHED".equals(value.getToStatus())
|
||||
&& "7".equals(value.getOperatorId()) && "status-1".equals(value.getIdempotencyKey())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullStatusLifecycleIsIdempotentAndCompletedCannotReverse() {
|
||||
LocalLooseboardOrder order = order(LocalLooseboardService.PENDING_ACCEPTANCE, 0L);
|
||||
when(orders.lockNo("100", "LB1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
var dispatch = new LocalLooseboardRequests.ChangeStatus(0L, "DISPATCHED", "dispatch-1", "dispatch");
|
||||
assertEquals(LocalLooseboardService.DISPATCHED,
|
||||
service.changeStatus("100", "LB1", "operator-7", dispatch).summary().status());
|
||||
assertEquals(1L, service.changeStatus("100", "LB1", "operator-7", dispatch).summary().version());
|
||||
assertThrows(ResponseStatusException.class, () -> service.changeStatus("100", "LB1", "operator-7",
|
||||
new LocalLooseboardRequests.ChangeStatus(1L, "IN_TRANSIT", "dispatch-1", "different")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.changeStatus("100", "LB1", "operator-7",
|
||||
new LocalLooseboardRequests.ChangeStatus(0L, "IN_TRANSIT", "stale-1", "stale")));
|
||||
assertEquals(LocalLooseboardService.IN_TRANSIT, service.changeStatus("100", "LB1", "operator-8",
|
||||
new LocalLooseboardRequests.ChangeStatus(1L, "IN_TRANSIT", "transit-1", "transit"))
|
||||
.summary().status());
|
||||
assertEquals(LocalLooseboardService.COMPLETED, service.changeStatus("100", "LB1", "operator-9",
|
||||
new LocalLooseboardRequests.ChangeStatus(2L, "COMPLETED", "complete-1", "complete"))
|
||||
.summary().status());
|
||||
assertThrows(ResponseStatusException.class, () -> service.changeStatus("100", "LB1", "operator-9",
|
||||
new LocalLooseboardRequests.ChangeStatus(3L, "IN_TRANSIT", "reverse-1", "reverse")));
|
||||
verify(histories, times(3)).insert(org.mockito.ArgumentMatchers.<LocalLooseboardHistory>argThat(value ->
|
||||
"STATUS".equals(value.getOperationType()) && value.getOperatorId() != null
|
||||
&& value.getIdempotencyKey() != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateDispatchedFixtureCanCancelButCannotProgressAfterCancellation() {
|
||||
LocalLooseboardOrder order = order(LocalLooseboardService.PENDING_ACCEPTANCE, 0L);
|
||||
when(orders.lockNo("100", "LB1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
assertEquals(LocalLooseboardService.DISPATCHED, service.changeStatus("100", "LB1", "operator-7",
|
||||
new LocalLooseboardRequests.ChangeStatus(0L, "DISPATCHED", "cancel-fixture-dispatch", "dispatch"))
|
||||
.summary().status());
|
||||
assertEquals(LocalLooseboardService.CANCELLED, service.changeStatus("100", "LB1", "operator-7",
|
||||
new LocalLooseboardRequests.ChangeStatus(1L, "CANCELLED", "cancel-1", "cancel"))
|
||||
.summary().status());
|
||||
assertThrows(ResponseStatusException.class, () -> service.changeStatus("100", "LB1", "operator-7",
|
||||
new LocalLooseboardRequests.ChangeStatus(2L, "IN_TRANSIT", "after-cancel", "invalid")));
|
||||
}
|
||||
|
||||
private LocalLooseboardRequests.Create create(String key) {
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.baiyue.lastleg.pda.warehouse;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.pda.warehouse.controller.PdaWarehouseController;
|
||||
import com.baiyue.lastleg.pda.warehouse.dto.PdaWarehouseRequests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class PdaWarehouseContractTest {
|
||||
@Test
|
||||
void requestsNeverAcceptServerOwnedIdentityOrResourceIds() {
|
||||
Set<String> forbidden = Set.of("customerId", "warehouseId", "planNo", "allocationId", "operatorId");
|
||||
for (Class<?> type : PdaWarehouseRequests.class.getDeclaredClasses()) {
|
||||
if (!type.isRecord()) continue;
|
||||
Set<String> fields = Arrays.stream(type.getRecordComponents()).map(RecordComponent::getName)
|
||||
.collect(Collectors.toSet());
|
||||
assertThat(fields).doesNotContainAnyElementsOf(forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void controllerRequiresPickerRoleAndPdaAuthority() {
|
||||
PreAuthorize permission = PdaWarehouseController.class.getAnnotation(PreAuthorize.class);
|
||||
assertThat(permission.value()).isEqualTo(
|
||||
"hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityRejectsUnboundOrNonPickerPlatformAccounts() {
|
||||
AuthenticatedUser unbound = user(Set.of(PdaWarehouseIdentity.PICKER_ROLE,
|
||||
PdaWarehouseIdentity.OPERATE_PERMISSION), null);
|
||||
assertThatThrownBy(() -> PdaWarehouseIdentity.require(unbound))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
|
||||
AuthenticatedUser admin = user(Set.of(PdaWarehouseIdentity.OPERATE_PERMISSION), 900L);
|
||||
assertThatThrownBy(() -> PdaWarehouseIdentity.require(admin))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
|
||||
assertThat(PdaWarehouseIdentity.require(user(Set.of(PdaWarehouseIdentity.PICKER_ROLE,
|
||||
PdaWarehouseIdentity.OPERATE_PERMISSION), 900L)).overseasWarehouseId()).isEqualTo(900L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void baselineContainsShippingGateAndRealOperationHistory() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql")).toLowerCase();
|
||||
assertThat(sql).contains("create table if not exists ll_container_shipping_gate")
|
||||
.contains("awaiting_pickup_ready")
|
||||
.contains("receiving_available")
|
||||
.contains("create table if not exists ll_pda_operation_history")
|
||||
.contains("unique key uk_ll_pda_history_idempotency(operator_key,idempotency_key)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiAndGatewayPublishOnlyStablePdaPaths() throws Exception {
|
||||
String openapi = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"));
|
||||
for (String path : Set.of("/api/pda/v1/last-leg/inventory/resolve:",
|
||||
"/api/pda/v1/last-leg/locations/resolve:", "/api/pda/v1/last-leg/pallets/resolve:",
|
||||
"/api/pda/v1/last-leg/operations:")) assertThat(openapi).contains(path);
|
||||
assertThat(openapi).doesNotContain("/api/pda/v1/last-leg/inbound-plans/{planNo}");
|
||||
String gateway = Files.readString(Path.of("../gateway-overseas/src/main/resources/application-overseas.yml"));
|
||||
assertThat(gateway).contains("/api/pda/v1/last-leg/**").contains("lb://lastleg-service");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(Set<String> authorities, Long warehouseId) {
|
||||
return new AuthenticatedUser(42L, 0L, 0L, 8L, "picker", "PLATFORM", "PDA",
|
||||
authorities, false, null, null, false, warehouseId);
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.lastleg.pda.warehouse.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.pda.warehouse.dto.PdaWarehouseRequests;
|
||||
import com.baiyue.lastleg.pda.warehouse.mapper.PdaWarehouseMapper;
|
||||
import com.baiyue.lastleg.pda.warehouse.model.PdaInventoryProjection;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PdaWarehouseServiceTest {
|
||||
@Mock PdaWarehouseMapper mapper;
|
||||
@Mock WarehouseResourceGateway warehouse;
|
||||
private PdaWarehouseService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new PdaWarehouseService();
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
ReflectionTestUtils.setField(service, "warehouse", warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inventoryScanIsBoundToTokenWarehouse() {
|
||||
when(mapper.scanInventory("900", "SKU-1")).thenReturn(List.of(inventory()));
|
||||
var result = service.inventory(900L, new PdaWarehouseRequests.Scan("SKU-1"));
|
||||
assertThat(result.items()).hasSize(1);
|
||||
assertThat(result.items().get(0).customerId()).isEqualTo("100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletFromAnotherWarehouseIsRejected() {
|
||||
when(warehouse.resolvePallet("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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void locationCombinesPhysicalCapacityAndInventoryOccupation() {
|
||||
when(warehouse.resolveLocation(900L, "A-01")).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseLocationReference(2L, "A-01", "STORAGE",
|
||||
new BigDecimal("1000"), "OCCUPIED"));
|
||||
when(mapper.inventoryByLocation("900", 2L, "A-01", "__NO_CARTON__"))
|
||||
.thenReturn(List.of(inventory()));
|
||||
var result = service.location(900L, new PdaWarehouseRequests.Scan("A-01"));
|
||||
assertThat(result.occupiedPackages()).isEqualTo(5);
|
||||
assertThat(result.inventoryPositionCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static PdaInventoryProjection inventory() {
|
||||
PdaInventoryProjection value = new PdaInventoryProjection(); value.setCustomerId("100");
|
||||
value.setBalanceId(10L); value.setPositionId(11L); value.setSkuCode("SKU-1");
|
||||
value.setInboundCode("IN-1"); value.setPalletId(12L); value.setPalletCode("PAL-1");
|
||||
value.setLocationId(2L); value.setLocationCode("A-01"); value.setAvailablePackages(5L);
|
||||
value.setHoldPackages(0L); value.setReceivingPackages(0L); value.setVersion(1L); value.setStatus("OCCUPIED");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+10
@@ -31,6 +31,16 @@ class OverseasProcurementOpenApiContractTest {
|
||||
}
|
||||
assertTrue(yaml.contains("platform:overseas-business:truck:create"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:truck:edit"));
|
||||
assertTrue(yaml.contains("/{orderNo}/review:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/receive-pallets:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/review-pallet-overage:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/allocate-pallets:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/prepare-dispatch:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/dispatch:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/start-transit:"));
|
||||
assertTrue(yaml.contains("/{orderNo}/complete:"));
|
||||
assertTrue(yaml.contains("enum: [PENDING_REVIEW, WAITING_INBOUND, RECEIVING, WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, PARTIALLY_ALLOCATED, ALLOCATED, PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, REJECTED, CANCELLED]"));
|
||||
assertTrue(yaml.contains("required: [version, status, idempotencyKey]"));
|
||||
assertTrue(yaml.contains("/api/platform/v1/last-leg/local-looseboard-orders:"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:looseboard:list"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:looseboard:create"));
|
||||
|
||||
+3
@@ -16,6 +16,9 @@ class OverseasProcurementSchemaContractTest {
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_status_history"));
|
||||
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
|
||||
assertTrue(sql.contains("review_idempotency_key VARCHAR(255) NULL"));
|
||||
assertTrue(sql.contains("review_request_hash CHAR(64) NULL"));
|
||||
assertTrue(sql.contains("idempotency_key VARCHAR(255) NULL COMMENT '操作幂等键'"));
|
||||
assertTrue(sql.contains("change_detail JSON NULL"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_outbound"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("outbound_quantity BIGINT"));
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ class OverseasProcurementControllerTest {
|
||||
|
||||
@Test
|
||||
void reviewRequiresExplicitPermission() {
|
||||
var request = new OverseasProcurementRequests.Review("APPROVE", null);
|
||||
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "review-1", null);
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> platform.review(user(0L, "PLATFORM", Set.of()), "100", "HC1", request));
|
||||
assertEquals(403, error.getStatusCode().value());
|
||||
@@ -56,7 +56,7 @@ class OverseasProcurementControllerTest {
|
||||
|
||||
@Test
|
||||
void platformReviewUsesExplicitTargetTenant() {
|
||||
var request = new OverseasProcurementRequests.Review("APPROVE", "ok");
|
||||
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "review-1", "ok");
|
||||
platform.review(user(0L, "PLATFORM", Set.of(OverseasProcurementService.REVIEW_PERMISSION)),
|
||||
"100", "HC1", request);
|
||||
verify(service).review("100", "HC1", "7", request);
|
||||
|
||||
+27
@@ -145,6 +145,33 @@ class OverseasProcurementServiceTest {
|
||||
assertEquals("Acme", values.get(0).customerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approveReviewIsIdempotentAuditedAndTerminalCannotReverse() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.PENDING_REVIEW, 0, 0);
|
||||
when(orders.lockNo("100", "HC1")).thenReturn(order);
|
||||
when(orders.findNo("100", "HC1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "approve-1", "approved");
|
||||
|
||||
var approved = service.review("100", "HC1", "operator-7", request);
|
||||
assertEquals(OverseasProcurementService.APPROVED, approved.summary().status());
|
||||
assertEquals(1L, approved.summary().version());
|
||||
assertEquals(1L, service.review("100", "HC1", "operator-7", request).summary().version());
|
||||
verify(histories).insert(org.mockito.ArgumentMatchers
|
||||
.<com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory>argThat(value ->
|
||||
"REVIEW".equals(value.getOperationType()) && "operator-7".equals(value.getOperatorId())
|
||||
&& "approve-1".equals(value.getIdempotencyKey())
|
||||
&& OverseasProcurementService.APPROVED.equals(value.getToStatus())));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
|
||||
new OverseasProcurementRequests.Review(1L, "REJECT", "approve-1", "different payload")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
|
||||
new OverseasProcurementRequests.Review(0L, "REJECT", "stale-review", "old version")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
|
||||
new OverseasProcurementRequests.Review(1L, "REJECT", "reject-after-approved", "reverse")));
|
||||
assertEquals(OverseasProcurementService.APPROVED, order.getStatus());
|
||||
}
|
||||
|
||||
private OverseasProcurementOrder order(String status, long inbound, long outbound) {
|
||||
OverseasProcurementOrder order = new OverseasProcurementOrder();
|
||||
order.setId(1L);
|
||||
|
||||
+11
-2
@@ -7,6 +7,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -15,6 +16,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SchemaChineseCommentContractTest {
|
||||
|
||||
private static final Set<String> MODULE_BASELINE_ONLY_TABLES = Set.of(
|
||||
"ll_us_truck_order_operation", "ll_local_looseboard_operation");
|
||||
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(ll_[a-z0-9_]+)"
|
||||
@@ -45,14 +49,16 @@ class SchemaChineseCommentContractTest {
|
||||
List<String> columns = List.of(
|
||||
"allocation_idempotency_key", "allocation_request_hash", "outbound_idempotency_key",
|
||||
"outbound_request_hash", "outbound_at", "pallet_count", "received_pallet_count",
|
||||
"allocated_pallet_count", "shipped_pallet_count", "receipt_idempotency_key",
|
||||
"receipt_request_hash"
|
||||
"allocated_pallet_count"
|
||||
);
|
||||
for (String column : columns) {
|
||||
assertColumnHasChineseComment(baseline, column, "基线 SQL");
|
||||
assertColumnHasChineseComment(incremental, column, "增量 SQL");
|
||||
assertIncrementalAddColumnHasChineseComment(incremental, column);
|
||||
}
|
||||
for (String column : List.of("review_idempotency_key", "review_request_hash", "result_version")) {
|
||||
assertColumnHasChineseComment(baseline, column, "基线 SQL");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TableDefinition> assertAllChineseComments(String sql, String source, int expectedCount) {
|
||||
@@ -90,6 +96,9 @@ class SchemaChineseCommentContractTest {
|
||||
assertTrue(marker >= 0, "增量 SQL 缺少 025 中文备注迁移");
|
||||
String migration = incremental.substring(marker);
|
||||
for (TableDefinition table : tables) {
|
||||
if (MODULE_BASELINE_ONLY_TABLES.contains(table.name())) {
|
||||
continue;
|
||||
}
|
||||
boolean createdInLegacy = hasCommentedCreate(migration, table.name());
|
||||
boolean createdInUnified = hasCommentedCreate(overseasUnified, table.name());
|
||||
assertTrue(createdInLegacy || createdInUnified
|
||||
|
||||
+3
-2
@@ -2,9 +2,10 @@ package com.baiyue.lastleg.service;
|
||||
import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.dropship.dto.DropshipRequests; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.fasterxml.jackson.databind.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*;
|
||||
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
||||
class LoadingCompletedConsumerTest {
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
|
||||
@Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),any(InventoryCommandRequests.InTransit.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
|
||||
@Test void loadingCreatesAwaitingPickupReadyGate(){ service.consume(event()); verify(shippingGates).initialize("book-1","100","","MSCU1",2L,1L); }
|
||||
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||
@Test void inboundSnapshotUsesCustomerIdAndHasNoPhantomTenantColumn() throws Exception {
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.baiyue.lastleg.shipping.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.FirstLegContainerShippingNodeRecordedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.entity.LastlegEventInbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventInboxMapper;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContainerShippingNodeConsumerTest {
|
||||
@Mock LastlegEventInboxMapper inboxes;
|
||||
@Mock ContainerShippingGateMapper gates;
|
||||
private final ObjectMapper json = new ObjectMapper().findAndRegisterModules();
|
||||
private ContainerShippingNodeConsumer consumer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
consumer = new ContainerShippingNodeConsumer();
|
||||
ReflectionTestUtils.setField(consumer, "inboxes", inboxes);
|
||||
ReflectionTestUtils.setField(consumer, "gates", gates);
|
||||
ReflectionTestUtils.setField(consumer, "objectMapper", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pickupReadyMonotonicallyOpensReceivingGate() {
|
||||
ContainerShippingGate gate = gate("AWAITING_PICKUP_READY", 70, 4, Instant.parse("2026-08-20T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
EventEnvelope event = event("123e4567-e89b-12d3-a456-426614174101", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
|
||||
assertThat(consumer.consume(event)).isEqualTo(1);
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getNodeSequence()).isEqualTo(80);
|
||||
assertThat(gate.getAggregateVersion()).isEqualTo(5);
|
||||
verify(gates).updateById(gate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowerNodeAndOlderAggregateAreAuditedWithoutRegression() {
|
||||
ContainerShippingGate gate = gate("RECEIVING_AVAILABLE", 90, 8, Instant.parse("2026-08-22T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
|
||||
consumer.consume(event("123e4567-e89b-12d3-a456-426614174102", "PICKUP_READY", 80, 7,
|
||||
Instant.parse("2026-08-23T10:00:00Z")));
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getNodeSequence()).isEqualTo(90);
|
||||
assertThat(gate.getAggregateVersion()).isEqualTo(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameNodeOnlyAcceptsLaterActualTimeAndEmptyReturnedNeverClosesGate() {
|
||||
ContainerShippingGate gate = gate("RECEIVING_AVAILABLE", 100, 9, Instant.parse("2026-08-22T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
consumer.consume(event("123e4567-e89b-12d3-a456-426614174103", "EMPTY_RETURNED", 100, 10,
|
||||
Instant.parse("2026-08-22T12:00:00Z")));
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getActualAt()).isEqualTo(java.time.LocalDateTime.parse("2026-08-22T12:00:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventIdIsIdempotentByHashAndRejectsDifferentHash() {
|
||||
EventEnvelope event = event("123e4567-e89b-12d3-a456-426614174104", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
LastlegEventInbox existing = new LastlegEventInbox(); existing.setPayloadHash(event.payloadHash());
|
||||
when(inboxes.selectById(event.eventId())).thenReturn(existing);
|
||||
assertThat(consumer.consume(event)).isEqualTo(1);
|
||||
verify(gates, never()).updateById(any(ContainerShippingGate.class));
|
||||
|
||||
existing.setPayloadHash("sha256:" + "0".repeat(64));
|
||||
assertThatThrownBy(() -> consumer.consume(event)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("摘要冲突");
|
||||
}
|
||||
|
||||
private ContainerShippingGate gate(String status, int sequence, long version, Instant actualAt) {
|
||||
ContainerShippingGate gate = new ContainerShippingGate(); gate.setContainerBookingId("CB-1");
|
||||
gate.setCustomerId("100"); gate.setOceanBookingId("OB-1"); gate.setContainerNo("CONT-1");
|
||||
gate.setStatus(status); gate.setNodeSequence(sequence); gate.setAggregateVersion(version);
|
||||
gate.setHandoverVersion(version + 1); gate.setActualAt(java.time.LocalDateTime.ofInstant(actualAt,
|
||||
java.time.ZoneOffset.UTC)); return gate;
|
||||
}
|
||||
|
||||
private EventEnvelope event(String eventId, String node, int sequence, long version, Instant actualAt) {
|
||||
FirstLegContainerShippingNodeRecordedV1 payload = new FirstLegContainerShippingNodeRecordedV1(
|
||||
"Q-1", "100", "OB-1", "CB-1", "CONT-1", node, sequence, actualAt,
|
||||
actualAt, "LAX", "Los Angeles", "CARRIER", "PE-1", node, "REF-1", "a".repeat(64));
|
||||
JsonNode payloadNode = json.valueToTree(payload);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
return new EventEnvelope(eventId, EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED, 1,
|
||||
actualAt, RegionCode.CN, RegionCode.OVERSEAS, "100", "CONTAINER_BOOKING", "CB-1",
|
||||
version, version + 1, "123e4567-e89b-12d3-a456-426614174199", "shipping:CB-1", null,
|
||||
payloadNode, hash);
|
||||
}
|
||||
}
|
||||
+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