Merge remote-tracking branch 'origin/test' into test

This commit is contained in:
2026-09-15 15:28:40 +08:00
193 changed files with 3807 additions and 943 deletions
@@ -6,7 +6,6 @@ import com.baiyue.lastleg.customervisit.mapper.CustomerVisitMapper;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
import com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -56,22 +55,12 @@ class CustomerVisitServiceTest {
}
@Test
void replaysDeliveredToDeliveredWithoutChangingDeliveryOrAddingHistory() {
DeliveryOrder delivery = delivery("DELIVERED");
CustomerVisit visit = new CustomerVisit();
visit.setId(33L);
visit.setStatus("PENDING");
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
when(visits.findByDelivery("1001", 11L)).thenReturn(visit);
void rejectsManualDeliveryCompletionBeforeLoadingTheOrder() {
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", 0L, null)))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("不支持");
var result = service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", 0L, null));
assertThat(result.replayed()).isTrue();
assertThat(result.deliveryStatus()).isEqualTo("DELIVERED");
assertThat(result.visitId()).isEqualTo(33L);
verify(deliveryOrders, never()).updateById(delivery);
verifyNoInteractions(deliveryOperations);
verifyNoInteractions(deliveryOrders, deliveryOperations);
}
@Test
@@ -89,26 +78,7 @@ class CustomerVisitServiceTest {
}
@Test
void completingDeliveryUpdatesOnlySubOrderStatusAndKeepsMainFlowStatus() {
DeliveryOrder delivery = delivery("DELIVERING");
delivery.setVersion(1L);
LastlegOrder tail = new LastlegOrder();
tail.setId(21L); tail.setCustomerId("1001"); tail.setCurrentStatus("INBOUND_COMPLETED");
CustomerVisit visit = new CustomerVisit(); visit.setId(33L); visit.setStatus("PENDING");
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
when(lastMileOrders.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(java.util.List.of(tail));
when(visits.findByDelivery("1001", 11L)).thenReturn(visit);
var result = service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", 1L, null));
assertThat(result.replayed()).isFalse();
assertThat(tail.getCurrentStatus()).isEqualTo("INBOUND_COMPLETED");
assertThat(tail.getSubOrderStatus()).isEqualTo("DELIVERY:DELIVERED");
}
@Test
void startMeansDeliveringAndOnlyCompleteSignsAndCreatesVisit() {
void startMeansDeliveringWithoutSigningOrCreatingVisit() {
DeliveryOrder delivery = delivery("WAIT_DELIVERY"); delivery.setVersion(0L);
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
var started = service.updateDeliveryStatus("1001", "9001", "DL-1",
@@ -118,17 +88,7 @@ class CustomerVisitServiceTest {
assertThat(delivery.getDispatchedAt()).isNotNull();
assertThat(delivery.getSignedAt()).isNull();
verify(visits, never()).insert(org.mockito.ArgumentMatchers.any(CustomerVisit.class));
CustomerVisit visit = new CustomerVisit(); visit.setId(33L); visit.setStatus("PENDING");
when(visits.findByDelivery("1001", 11L)).thenReturn(visit);
var completed = service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", 1L, null));
assertThat(completed.deliveryStatus()).isEqualTo("DELIVERED");
assertThat(delivery.getSignedAt()).isNotNull();
assertThat(delivery.getVersion()).isEqualTo(2);
assertThat(completed.visitId()).isEqualTo(33);
assertThat(service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", 1L, null)).replayed()).isTrue();
verify(deliveryOrders, org.mockito.Mockito.times(2)).updateById(delivery);
verify(deliveryOrders).updateById(delivery);
}
private DeliveryOrder delivery(String status) {
@@ -67,6 +67,6 @@ class DeliveryLongIdJsonContractTest {
null, null, "PS-1", null, null, null, null, "CREATED", null,
null, null, null, null, null, null, null, null, null, null, null,
0, 0, null, null, null, null, null, null, null, null, 0, java.util.List.of(), null,
com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(null));
com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(null), null, null, null);
}
}
@@ -25,6 +25,7 @@ class DeliverySchemaContractTest {
assertTrue(schema.contains("create table if not exists ll_outbound_order"));
assertTrue(schema.contains("status varchar(32) not null default 'wait_outbound'"));
assertTrue(schema.contains("unique key uk_ll_outbound_idempotency (customer_id,idempotency_key)"));
assertTrue(schema.contains("request_hash char(64) null"));
assertTrue(schema.contains("unique key uk_ll_outbound_dispatch (customer_id,dispatch_order_id)"));
assertTrue(schema.contains("create table if not exists ll_outbound_pallet_item"));
assertTrue(schema.contains("unique key uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code)"));
@@ -34,7 +35,9 @@ class DeliverySchemaContractTest {
assertTrue(schema.contains("create table if not exists ll_delivery_order"));
assertTrue(schema.contains("status varchar(32) not null default 'wait_delivery'"));
assertTrue(schema.contains("unique key uk_ll_delivery_idempotency (customer_id,idempotency_key)"));
assertTrue(schema.contains("unique key uk_ll_delivery_outbound (customer_id,outbound_order_id)"));
assertTrue(schema.contains("unique key uk_ll_delivery_outbound_destination (customer_id,outbound_order_id,delivery_group_key)"));
assertTrue(schema.contains("destination_warehouse_code varchar(128) not null"));
assertTrue(schema.contains("delivery_group_key varchar(255) not null"));
}
@Test
@@ -52,7 +55,8 @@ class DeliverySchemaContractTest {
assertTrue(createEnd > create);
String definition = schema.substring(create, createEnd);
for (String column : new String[] {
"overseas_warehouse_name", "customer_name", "waybill_no", "delivery_method",
"overseas_warehouse_name", "customer_name", "destination_warehouse_id",
"destination_warehouse_code", "destination_warehouse_name", "delivery_group_key", "waybill_no", "delivery_method",
"carrier_name", "pickup_method", "loading_type", "vehicle_type", "vehicle_size",
"vehicle_plate", "driver_name", "driver_phone", "appointment_account", "seller_name",
"total_weight_kg", "total_volume_cbm", "planned_delivery_at", "dispatched_at",
@@ -66,6 +70,7 @@ class DeliverySchemaContractTest {
StandardCharsets.UTF_8).toLowerCase();
assertTrue(overseas.contains("begin inline empty-database baseline: lastleg"));
assertTrue(overseas.contains(createToken));
assertTrue(overseas.contains("request_hash char(64) null"));
assertTrue(overseas.indexOf(createToken) < overseas.indexOf("assert_phase5_lastleg_identifiers_migratable"));
assertTrue(overseas.contains("alter table ll_delivery_order add column overseas_warehouse_name"));
}
@@ -19,6 +19,7 @@ class DropshipOutboundCandidateMapperContractTest {
assertTrue(sql.contains(token), token);
}
assertTrue(sql.contains("LEAST(pos.available_packages,s.package_count"));
assertTrue(sql.contains("#{customerId} IS NULL OR #{customerId}='' OR p.customer_id=#{customerId}"));
}
@Test
@@ -36,6 +36,37 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class PlatformDispatchOutboundContractTest {
@Test
void candidateHttpQueryAcceptsWarehouseWithoutCustomerAndPreservesExplicitFilter() throws Exception {
DeliveryTransferController controller = new DeliveryTransferController();
var service = mock(com.baiyue.lastleg.delivery.service.DeliveryTransferService.class);
ReflectionTestUtils.setField(controller, "service", service);
MockMvc mvc = MockMvcBuilders.standaloneSetup(controller)
.setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()).build();
AuthenticatedUser platform = new AuthenticatedUser(1000001L, 0L, 0L, 1L, "platform_admin",
"PLATFORM", "WEB", Set.of(OverseasPermissions.OUTBOUND_CREATE));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(platform, null, Set.of()));
String path = "/api/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates";
try {
mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isOk());
org.mockito.Mockito.verify(service).oneClickCandidates(null, "WH-1");
mvc.perform(get(path).param("customerId", "101").param("warehouseId", "WH-1"))
.andExpect(status().isOk());
org.mockito.Mockito.verify(service).oneClickCandidates("101", "WH-1");
SecurityContextHolder.clearContext();
mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isUnauthorized());
AuthenticatedUser merchant = new AuthenticatedUser(7L, 100L, 100L, 8L, "merchant",
"MERCHANT", "WEB", Set.of(OverseasPermissions.OUTBOUND_CREATE));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(merchant, null, Set.of()));
mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isForbidden());
org.mockito.Mockito.verifyNoMoreInteractions(service);
} finally {
SecurityContextHolder.clearContext();
}
}
@Test
void outboundDetailAcceptsTheDocumentUrlWithoutPagingParameters() throws Exception {
DeliveryTransferController controller = new DeliveryTransferController();
@@ -80,14 +111,14 @@ class PlatformDispatchOutboundContractTest {
}
assertTrue(Arrays.stream(DispatchController.class.getDeclaredMethods())
.noneMatch(method -> "depart".equals(method.getName())));
assertEquals(Set.of("vehiclePlate", "driverName", "driverPhone", "carrierName", "route",
assertEquals(Set.of("vehiclePlate", "driverName", "driverPhone", "carrierName",
"fbaAppointmentNo", "fbaAppointmentStatus", "vehicleAppointmentStatus", "estimatedArrivalAt"),
Arrays.stream(DispatchRequests.Update.class.getRecordComponents())
.map(component -> component.getName()).collect(java.util.stream.Collectors.toSet()));
}
@Test
void oneClickRequiresCustomerWhileOutboundListKeepsOptionalFilter() throws Exception {
void oneClickCreationRequiresCustomerWhileCandidateAndListFiltersAreOptional() throws Exception {
Method create = DeliveryTransferController.class.getMethod("createOneClick", AuthenticatedUser.class,
String.class, DeliveryRequests.OneClickOutbound.class);
Method candidates = DeliveryTransferController.class.getMethod("oneClickCandidates", AuthenticatedUser.class,
@@ -102,7 +133,7 @@ class PlatformDispatchOutboundContractTest {
RequestParam pageCustomer = page.getParameters()[1].getAnnotation(RequestParam.class);
assertNotNull(createCustomer);
assertTrue(createCustomer.required());
assertTrue(candidates.getParameters()[1].getAnnotation(RequestParam.class).required());
assertFalse(candidates.getParameters()[1].getAnnotation(RequestParam.class).required());
assertFalse(candidates.getParameters()[2].getAnnotation(RequestParam.class).required());
assertNotNull(pageCustomer);
assertFalse(pageCustomer.required());
@@ -181,6 +212,7 @@ class PlatformDispatchOutboundContractTest {
assertTrue(yaml.contains("platform:overseas-warehouse:outbound:list"));
assertTrue(yaml.contains("platform:overseas-warehouse:outbound:create"));
assertTrue(yaml.contains("platform:overseas-warehouse:outbound:cancel"));
assertTrue(yaml.contains("vehicleSize: { type: string, nullable: true, enum: [15托, 30托] }"));
}
private static void assertPermission(Method method, String permission) {
@@ -8,10 +8,11 @@ class DeliveryStatusPolicyTest {
@Test
void exposesOnlyServerAuthorizedActionsForEachState() {
assertThat(DeliveryStatusPolicy.allowedActions("WAIT_DELIVERY"))
.containsExactly("START_DELIVERY", "COMPLETE_DELIVERY", "REJECT_DELIVERY", "CANCEL_DELIVERY");
.containsExactly("START_DELIVERY", "REJECT_DELIVERY", "CANCEL_DELIVERY");
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERING"))
.containsExactly("COMPLETE_DELIVERY", "REJECT_DELIVERY", "MARK_RETURN");
.containsExactly("REJECT_DELIVERY", "MARK_RETURN");
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERED")).containsExactly("MARK_RETURN");
assertThat(DeliveryStatusPolicy.isEditableTarget("DELIVERED")).isFalse();
assertThat(DeliveryStatusPolicy.allowedActions("RETURN_PENDING")).isEmpty();
assertThat(DeliveryStatusPolicy.allowedActions("CANCELLED")).isEmpty();
}
@@ -0,0 +1,59 @@
package com.baiyue.lastleg.delivery.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.OverseasDeliveryCompletedV1;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
class DeliveryCompletedOutboxServiceTest {
@Test
void emitsOneEventPerSourceInboundAcrossFbaWarehouses() throws Exception {
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
ObjectMapper json = new ObjectMapper().registerModule(new JavaTimeModule());
DeliveryCompletedOutboxService service = new DeliveryCompletedOutboxService();
ReflectionTestUtils.setField(service, "outbox", mapper);
ReflectionTestUtils.setField(service, "objectMapper", json);
DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(1L); delivery.setDeliveryNo("DL-1"); delivery.setVersion(2L);
service.publish(delivery, List.of(order(11, "101", "201", "IN-1", "ONT8"),
order(12, "102", "202", "IN-2", "ACY2")), Instant.parse("2026-09-15T08:00:00Z"));
ArgumentCaptor<LastlegEventOutbox> rows = ArgumentCaptor.forClass(LastlegEventOutbox.class);
verify(mapper, times(2)).insert(rows.capture());
List<OverseasDeliveryCompletedV1> payloads = rows.getAllValues().stream().map(row -> {
try {
EventEnvelope envelope = json.readValue(row.getPayloadJson(), EventEnvelope.class);
return json.treeToValue(envelope.payload(), OverseasDeliveryCompletedV1.class);
} catch (Exception exception) { throw new RuntimeException(exception); }
}).toList();
assertThat(payloads).extracting(OverseasDeliveryCompletedV1::sourceInboundId)
.containsExactly("101", "102");
assertThat(payloads).extracting(OverseasDeliveryCompletedV1::fbaWarehouseCode)
.containsExactly("ONT8", "ACY2");
}
private LastlegOrder order(long id, String inboundId, String orderId, String inboundCode, String fba) {
LastlegOrder value = new LastlegOrder();
value.setId(id); value.setOrderNo("LM-" + id); value.setCustomerId("88");
value.setSourceInboundId(inboundId); value.setInboundCode(inboundCode);
value.setSourceOrderId(orderId); value.setSourceOrderNo("FL-" + orderId);
value.setRootOrderId(orderId); value.setFbaWarehouseCode(fba);
return value;
}
}
@@ -5,6 +5,9 @@ import com.baiyue.lastleg.delivery.entity.*;
import com.baiyue.lastleg.delivery.gateway.DeliveryDocumentFileGateway;
import com.baiyue.lastleg.delivery.mapper.*;
import com.baiyue.lastleg.delivery.model.*;
import com.baiyue.lastleg.customervisit.service.CustomerVisitService;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -26,6 +29,9 @@ class DeliveryDocumentServiceTest {
@Mock DeliveryDocumentRevisionMapper revisions;
@Mock DeliveryOperationMapper operations;
@Mock DeliveryDocumentFileGateway files;
@Mock LastlegOrderMapper lastlegOrders;
@Mock DeliveryCompletedOutboxService deliveryCompletedOutbox;
@Mock CustomerVisitService customerVisits;
DeliveryDocumentService service;
DeliveryOrder order;
@BeforeEach void setUp() {
@@ -35,6 +41,9 @@ class DeliveryDocumentServiceTest {
ReflectionTestUtils.setField(service,"operations",operations);
ReflectionTestUtils.setField(service,"files",files);
ReflectionTestUtils.setField(service,"objectMapper",new ObjectMapper().findAndRegisterModules());
ReflectionTestUtils.setField(service,"lastlegOrders",lastlegOrders);
ReflectionTestUtils.setField(service,"deliveryCompletedOutbox",deliveryCompletedOutbox);
ReflectionTestUtils.setField(service,"customerVisits",customerVisits);
order = new DeliveryOrder(); order.setId(10L); order.setCustomerId("100");
order.setDeliveryNo("DL-1"); order.setVersion(0L); order.setStatus("DELIVERED");
}
@@ -121,6 +130,27 @@ class DeliveryDocumentServiceTest {
assertThat(result.uploadedCategoryCount()).isEqualTo(3); assertThat(result.totalCategoryCount()).isEqualTo(3);
assertThat(result.categories()).allMatch(c->c.status().equals("UPLOADED"));
}
@Test void savingReceiptAutomaticallyCompletesOnlyThisDeliveryAndItsBoundOrders() {
order.setStatus("WAIT_DELIVERY");
LastlegOrder related = new LastlegOrder(); related.setId(20L); related.setCustomerId("100");
related.setDeliveryOrderNo("DL-1"); related.setSubOrderStatus("DELIVERY:WAIT_DELIVERY");
when(orders.lockByNo("100","DL-1")).thenReturn(order);
when(files.metadata("100",10,RECEIPT,13)).thenReturn(file(13));
when(lastlegOrders.selectList(any())).thenReturn(List.of(related));
service.save("100","DL-1","9",request(0,"receipt",List.of(new Binding(RECEIPT,List.of(13L)))));
assertThat(order.getStatus()).isEqualTo("DELIVERED");
assertThat(order.getSignedAt()).isNotNull();
assertThat(related.getSubOrderStatus()).isEqualTo("DELIVERY:DELIVERED");
verify(lastlegOrders).updateById(related);
verify(deliveryCompletedOutbox).publish(eq(order), eq(List.of(related)), any());
verify(customerVisits).ensureForCompletedDelivery(eq(order), any());
ArgumentCaptor<DeliveryOperation> operation = ArgumentCaptor.forClass(DeliveryOperation.class);
verify(operations, times(2)).insert(operation.capture());
assertThat(operation.getAllValues()).extracting(DeliveryOperation::getOperationNode)
.containsExactly("SAVE_DOCUMENTS", "RECEIPT_AUTO_COMPLETE");
}
private Save request(long version,String key,List<Binding> bindings) { return new Save(version,key,"BOL-1",bindings); }
private File file(long id) { return new File(id,"a.pdf","application/pdf",1,LocalDateTime.of(2026,9,9,8,0)); }
}
@@ -91,6 +91,7 @@ class DeliveryTransferServiceTest {
@BeforeEach
void setUp() {
service = new DeliveryTransferService();
ReflectionTestUtils.setField(service, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
ReflectionTestUtils.setField(service, "dispatchOrders", dispatchOrders);
ReflectionTestUtils.setField(service, "dispatchItems", dispatchItems);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
@@ -116,7 +117,7 @@ class DeliveryTransferServiceTest {
}
@Test
void firstTransferCreatesOneOutboundOneDeliveryAndAllPalletSnapshots() {
void firstTransferCreatesOneOutboundAndOneDeliveryPerDestinationFbaWarehouse() {
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
when(dispatchOrders.lockGlobalByDispatchNo("DSP-101")).thenReturn(dispatch);
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(
@@ -131,7 +132,7 @@ class DeliveryTransferServiceTest {
ArgumentCaptor<DeliveryOrder> delivery = ArgumentCaptor.forClass(DeliveryOrder.class);
ArgumentCaptor<OutboundPalletItem> pallets = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundOrders).insert(outbound.capture());
verify(deliveryOrders).insert(delivery.capture());
verify(deliveryOrders, org.mockito.Mockito.times(2)).insert(delivery.capture());
verify(outboundPallets, org.mockito.Mockito.times(2)).insert(pallets.capture());
assertEquals("WAIT_OUTBOUND", outbound.getValue().getStatus());
@@ -139,15 +140,18 @@ class DeliveryTransferServiceTest {
assertEquals(20, outbound.getValue().getPackageCount());
assertEquals("transfer-101", outbound.getValue().getIdempotencyKey());
assertTrue(outbound.getValue().getOutboundNo().matches("OB-WH-\\d{8}-\\d{6}"));
assertEquals(outbound.getValue().getId(), delivery.getValue().getOutboundOrderId());
assertEquals("WAIT_DELIVERY", delivery.getValue().getStatus());
assertTrue(delivery.getValue().getDeliveryNo().matches("DL-WH-\\d{8}-\\d{6}"));
assertTrue(delivery.getAllValues().stream().allMatch(value -> outbound.getValue().getId().equals(value.getOutboundOrderId())));
assertEquals(List.of("ONT8", "LAX9"), delivery.getAllValues().stream().map(DeliveryOrder::getDestinationWarehouseCode).toList());
assertEquals(List.of(12, 8), delivery.getAllValues().stream().map(DeliveryOrder::getPackageCount).toList());
assertTrue(delivery.getAllValues().stream().allMatch(value -> "WAIT_DELIVERY".equals(value.getStatus())));
assertEquals(List.of("ONT8", "LAX9"), pallets.getAllValues().stream()
.map(OutboundPalletItem::getDestinationWarehouseCode).toList());
assertEquals(List.of("IN-A", "IN-B"), pallets.getAllValues().stream()
.map(OutboundPalletItem::getInboundCode).toList());
assertEquals(20, result.packageCount());
assertEquals(2, result.palletCount());
assertEquals(2, result.deliveries().size());
verify(feeSources, org.mockito.Mockito.times(1)).publishApproved(any(), any(), any());
assertTrue(!result.replayed());
}
@@ -192,14 +196,14 @@ class DeliveryTransferServiceTest {
DeliveryOrder delivery = delivery(401L, outbound, "DEL-401");
when(dispatchOrders.lockGlobalByDispatchNo("DSP-101")).thenReturn(dispatch);
when(outboundOrders.findByIdempotency("TENANT-A", "transfer-101")).thenReturn(outbound);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(List.of(delivery));
DeliveryResponses.TransferResult result = service.platformTransfer("DSP-101", "U-1",
transferRequest(" transfer-101 ", null));
assertTrue(result.replayed());
assertEquals("OUT-301", result.outboundNo());
assertEquals("DEL-401", result.deliveryNo());
assertEquals("DEL-401", result.deliveries().get(0).deliveryNo());
verify(outboundOrders, never()).insert((OutboundOrder) any());
verify(outboundPallets, never()).insert((OutboundPalletItem) any());
verify(deliveryOrders, never()).insert((DeliveryOrder) any());
@@ -229,14 +233,14 @@ class DeliveryTransferServiceTest {
item(201L, 501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12)));
when(outboundOrders.insert(any(OutboundOrder.class))).thenThrow(new org.springframework.dao.DuplicateKeyException("duplicate"));
when(outboundOrders.findByIdempotency("TENANT-A", "transfer-101")).thenReturn(null, winner);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(List.of(delivery));
DeliveryResponses.TransferResult result = service.platformTransfer("DSP-101", "U-1",
transferRequest("transfer-101", null));
assertTrue(result.replayed());
assertEquals("OUT-301", result.outboundNo());
assertEquals("DEL-401", result.deliveryNo());
assertEquals("DEL-401", result.deliveries().get(0).deliveryNo());
verify(outboundPallets, never()).insert((OutboundPalletItem) any());
verify(deliveryOrders, never()).insert((DeliveryOrder) any());
}
@@ -291,7 +295,7 @@ class DeliveryTransferServiceTest {
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
assertEquals("OUTBOUNDED", detail.outbound().status());
org.junit.jupiter.api.Assertions.assertNull(detail.delivery());
org.junit.jupiter.api.Assertions.assertTrue(detail.deliveries().isEmpty());
assertEquals("IN-A", detail.pallets().get(0).inboundCode());
assertEquals(4, detail.pallets().get(0).packageCount());
}
@@ -311,13 +315,13 @@ class DeliveryTransferServiceTest {
OutboundPalletItem pallet = pallet(501L, "IP-ONT8-20260907-000001-P001", "ONT8", "Ontario", "IN-A", 12);
pallet.setSkuCode("SKU-1");
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(List.of(delivery));
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(pallet));
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
assertEquals("OUT-301", detail.outbound().outboundNo());
assertEquals("DEL-401", detail.delivery().deliveryNo());
assertEquals("DEL-401", detail.deliveries().get(0).deliveryNo());
assertEquals(1, detail.pallets().size());
assertEquals("IP-ONT8-20260907-000001-P001", detail.pallets().get(0).palletNo());
assertEquals("IN-A", detail.pallets().get(0).inboundCode());
@@ -332,7 +336,7 @@ class DeliveryTransferServiceTest {
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "transfer-101");
DeliveryOrder delivery = delivery(401L, outbound, "DEL-401");
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(List.of(delivery));
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
@@ -360,13 +364,13 @@ class DeliveryTransferServiceTest {
when(dispatchOrders.findByDispatchNo("TENANT-A", "DSP-101")).thenReturn(dispatch);
when(dispatchOrders.lockById("TENANT-A", 101L)).thenReturn(dispatch);
when(outboundOrders.findByIdempotency("TENANT-A", "merchant-key")).thenReturn(outbound);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(List.of(delivery));
DeliveryResponses.TransferResult result = service.merchantTransfer("TENANT-A", "DSP-101", "U-2",
transferRequest(" merchant-key ", null));
assertTrue(result.replayed());
assertEquals("DEL-401", result.deliveryNo());
assertEquals("DEL-401", result.deliveries().get(0).deliveryNo());
verify(outboundOrders, never()).insert((OutboundOrder) any());
}
@@ -395,6 +399,24 @@ class DeliveryTransferServiceTest {
verify(dispatchItems, never()).findByDispatchOrder(any(), any());
}
@Test
void deliveryDetailAndBolSourceRowsContainOnlyItsDestinationFbaWarehouse() {
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "transfer-101");
DeliveryOrder ont8 = delivery(401L, outbound, "DEL-ONT8");
DispatchItem ont8Item = item(201L, 501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12);
ont8Item.setOrderId(901L);
DispatchItem lax9Item = item(202L, 502L, "PLT-502", "LAX9", "Los Angeles", "IN-B", 8);
lax9Item.setOrderId(902L);
when(deliveryOrders.findByNo("TENANT-A", "DEL-ONT8")).thenReturn(ont8);
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(ont8Item, lax9Item));
when(tailOrders.selectBatchIds(List.of(901L))).thenReturn(List.of());
DeliveryResponses.DeliveryDetail detail = service.deliveryDetail("TENANT-A", "DEL-ONT8");
assertEquals("ONT8", detail.order().destinationWarehouseCode());
verify(tailOrders).selectBatchIds(List.of(901L));
}
@Test
void globalOutboundCancelIsIdempotentForAnUnexecutedOrder() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");
@@ -440,6 +462,7 @@ class DeliveryTransferServiceTest {
.thenReturn(List.of(new DropshipCarton()));
InventoryPosition position = new InventoryPosition();
position.setId(900L + i); position.setCustomerId("TENANT-A"); position.setBalanceId(800L + i);
position.setOverseasWarehouseId("WH-1");
position.setPalletId(501L); position.setPalletNo("DP-SHARED");
InventoryBalance balance = new InventoryBalance();
balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-" + i);
@@ -460,6 +483,10 @@ class DeliveryTransferServiceTest {
@Test
void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() {
var generator = org.mockito.Mockito.mock(LastlegOrderNoGenerator.class);
ReflectionTestUtils.setField(service, "orderNoGenerator", generator);
InventoryPosition position = new InventoryPosition(); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
when(inventoryPositions.selectById(901L)).thenReturn(position);
DropshipOutboundCandidate candidate = dropshipCandidate(901L, 1001L, "WH-1", "PLT-501", "MIXED", 1L);
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 901L, "MIXED")).thenReturn(candidate);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "MIXED", 1L)).thenReturn(List.of());
@@ -470,6 +497,45 @@ class DeliveryTransferServiceTest {
assertEquals(409, exception.getStatusCode().value());
assertTrue(exception.getReason().contains("一件代发可用箱数不足"));
verify(outboundPallets, never()).insert(any(OutboundPalletItem.class));
}
@Test
void concurrentOneClickCreateReplaysCurrentCommittedRowBeforeTakingInventoryLocks() {
var generator = org.mockito.Mockito.mock(LastlegOrderNoGenerator.class);
ReflectionTestUtils.setField(service, "orderNoGenerator", generator);
var request = new DeliveryRequests.OneClickOutbound("recipient", List.of(
new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "B"))), "remark", "race-key");
OutboundOrder winner = outbound(301L, null, null, "OUT-WINNER", "race-key");
winner.setOutboundType("ONE_CLICK_DROPSHIP"); winner.setRequestHash(service.oneClickRequestHash(request));
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setTaskNo("PDA-WINNER"); task.setStatus("PENDING");
InventoryPosition position = new InventoryPosition(); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(outboundOrders.insert(any(OutboundOrder.class))).thenThrow(new org.springframework.dao.DuplicateKeyException("race"));
when(outboundOrders.lockByIdempotency("TENANT-A", "race-key")).thenReturn(winner);
when(pdaTasks.lockByOutbound("TENANT-A", 301L)).thenReturn(task);
assertEquals("PDA-WINNER", service.createOneClick("TENANT-A", "U-1", request).taskNo());
verify(dropshipProfiles, never()).lockOutboundCandidate(any(), any(), any());
verify(outboundPallets, never()).insert(any(OutboundPalletItem.class));
verify(pdaTasks, never()).insert(any(OutboundPdaTask.class));
}
@Test
void oneClickReplayRejectsChangedRecipientItemsMarksAndRemark() {
var item = new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "B"));
var request = new DeliveryRequests.OneClickOutbound("recipient", List.of(item), "remark", "key");
OutboundOrder existing = outbound(301L, null, null, "OUT-1", "key");
existing.setOutboundType("ONE_CLICK_DROPSHIP"); existing.setRequestHash(service.oneClickRequestHash(request));
when(outboundOrders.findByIdempotency("TENANT-A", "key")).thenReturn(existing);
for (var changed : List.of(
new DeliveryRequests.OneClickOutbound("changed", List.of(item), "remark", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(item), "changed", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(new DeliveryRequests.OneClickItem(902L, 2L, "SKU-1", List.of("A", "B"))), "remark", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "C"))), "remark", "key"))) {
assertEquals(409, assertThrows(ResponseStatusException.class,
() -> service.createOneClick("TENANT-A", "U-1", changed)).getStatusCode().value());
}
verify(inventoryPositions, never()).selectById(any());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
}
@@ -491,9 +557,22 @@ class DeliveryTransferServiceTest {
verify(inventoryPositions, never()).selectById(any());
}
@Test
void oneClickCandidatesAllowPlatformWarehouseQueryWithoutCustomer() {
DropshipOutboundCandidate first = dropshipCandidate(901L, 1001L, "WH-1", "DS-A001", "SKU-1", 1L);
DropshipOutboundCandidate second = dropshipCandidate(902L, 1002L, "WH-1", "DS-A002", "SKU-1", 3L);
when(dropshipProfiles.listOutboundCandidates(null, "WH-1")).thenReturn(List.of(first, second));
var result = service.oneClickCandidates(null, " WH-1 ");
assertEquals(2, result.size());
assertEquals("DS-A001", result.get(0).palletNo());
assertEquals("DS-A002", result.get(1).palletNo());
verify(dropshipProfiles).listOutboundCandidates(null, "WH-1");
}
@Test
void oneClickCreationRejectsOrdinaryInventoryBeforeAnyWrite() {
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 901L, "MIXED")).thenReturn(null);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick(
"TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of(
@@ -619,6 +698,12 @@ class DeliveryTransferServiceTest {
assertEquals("SKU-1", projected.skuCode());
assertEquals("IN-A", projected.inboundCode());
assertEquals(1L, projected.availablePackages());
item.setCartonMarks("CTN-B");
assertEquals(List.of("CTN-B"), service.pdaTaskDetail("WH-1", "PDA-701").items().get(0).positions()
.stream().map(DeliveryResponses.PdaPickPosition::cartonMark).toList());
second.setStatus("SHIPPED");
assertTrue(service.pdaTaskDetail("WH-1", "PDA-701").items().get(0).positions().isEmpty());
}
@Test
@@ -647,6 +732,7 @@ class DeliveryTransferServiceTest {
DeliveryResponses.PdaTaskDetail detail = service.pdaTaskDetail("WH-1", "PDA-701");
assertEquals("CTN-A", detail.items().get(0).positions().get(0).cartonMark());
assertEquals("SKU-1", detail.items().get(0).positions().get(0).skuCode());
}
@Test
@@ -658,13 +744,15 @@ class DeliveryTransferServiceTest {
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = pallet(501L, "DP-1", "ONT8", "Ontario", "IN-A", 2);
item.setSkuCode("SKU-1");
item.setSkuCode("SKU-1"); item.setCartonMarks("BOX-A,BOX-B");
InventoryPosition position = new InventoryPosition(); position.setId(901L); position.setCustomerId("TENANT-A");
position.setBalanceId(801L); position.setPalletId(501L); position.setOverseasWarehouseId("WH-1");
InventoryBalance balance = new InventoryBalance(); balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-A");
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
DropshipCarton first = new DropshipCarton(); first.setId(1101L); first.setProductQuantity(7L);
DropshipCarton second = new DropshipCarton(); second.setId(1102L); second.setProductQuantity(13L);
first.setStatus("AVAILABLE"); first.setSkuCode("SKU-1"); first.setCartonMark("BOX-A");
second.setStatus("AVAILABLE"); second.setSkuCode("SKU-1"); second.setCartonMark("BOX-B");
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
@@ -672,12 +760,18 @@ class DeliveryTransferServiceTest {
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(inventoryBalances.selectById(801L)).thenReturn(balance);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "SKU-1", 1L)).thenReturn(List.of(first), List.of(second));
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-A")).thenReturn(first);
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-B")).thenReturn(second);
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L)).thenReturn(1);
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L)).thenReturn(1);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-7"));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13"));
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "wrong-mark", List.of("WRONG-BOX"))));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-7", List.of("BOX-A")));
assertEquals(1, item.getPickedPackageCount()); assertEquals("AVAILABLE", second.getStatus());
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "repeat-box-new-key", List.of("BOX-A"))));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13", List.of("BOX-B")));
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L);
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L);
assertEquals("SHIPPED", first.getStatus()); assertEquals("SHIPPED", second.getStatus());
@@ -779,6 +873,8 @@ class DeliveryTransferServiceTest {
value.setOutboundOrderId(outbound.getId()); value.setOutboundNo(outbound.getOutboundNo());
value.setDispatchOrderId(outbound.getDispatchOrderId()); value.setDispatchNo(outbound.getDispatchNo());
value.setOverseasWarehouseId(outbound.getOverseasWarehouseId()); value.setStatus("WAIT_DELIVERY");
value.setDestinationWarehouseId("DEST-ONT8"); value.setDestinationWarehouseCode("ONT8");
value.setDestinationWarehouseName("Ontario"); value.setDeliveryGroupKey("CODE:ONT8");
value.setPalletCount(outbound.getPalletCount()); value.setPackageCount(outbound.getPackageCount());
value.setCreatedAt(LocalDateTime.now());
return value;
@@ -25,11 +25,18 @@ class SmartDispatchOpenApiContractTest {
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/drafts/{draftToken}/containers/{containerId}:"));
assertTrue(yaml.contains("draftContainerId:"));
assertTrue(yaml.contains("SmartDispatchDraftContainer:"));
assertTrue(yaml.contains("capacity: { type: integer, enum: [15, 30] }"));
assertTrue(yaml.contains("SmartDispatchDraft:"));
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:list"));
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:smart-dispatch"));
assertTrue(yaml.contains("SmartDispatchPreArrange"));
assertTrue(yaml.contains("SmartDispatchCandidate"));
String preArrangeSchema = yaml.substring(yaml.indexOf(" SmartDispatchPreArrange:"),
yaml.indexOf(" SmartDispatchDraftPallet:"));
String updateSchema = yaml.substring(yaml.indexOf(" DispatchUpdate:"),
yaml.indexOf(" DeliveryTransfer:"));
assertFalse(preArrangeSchema.contains("route:"));
assertFalse(updateSchema.contains("route:"));
assertTrue(yaml.contains("name: planIds"));
String planIdsParameter = yaml.substring(yaml.indexOf("name: planIds"),
yaml.indexOf("name: orderNo", yaml.indexOf("name: planIds")));
@@ -53,7 +53,7 @@ class SmartDispatchControllerTest {
@Test
void preArrangeResolvesSingleCustomerAndAuthenticatedOperator() {
DispatchRequests.PreArrange request = new DispatchRequests.PreArrange(
List.of(10L, 11L), "ONT8 -> LAX9", "APT-1", "SUCCESS", "PENDING");
List.of(10L, 11L), "APT-1", "SUCCESS", "PENDING");
when(service.resolveCustomerScopeForPallets(List.of(10L, 11L))).thenReturn("100");
controller.preArrange(platform(), request);
@@ -75,4 +75,34 @@ class DispatchBolPrintServiceTest {
assertThat(document.shipFromAddress()).isEqualTo(
"10767 7th Street, Rancho Cucamonga, California 91730, US"));
}
@Test
void createsOneBolDocumentPerFbaWarehouse() {
String warehouseId = "193000000000200003";
DispatchResponse detail = new DispatchResponse(1L, "DS-WH-20260911-624449", "DEV-23653906",
null, null, "五月开发验收车队", null, null, warehouseId, "洛杉矶仓", null,
null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE",
2, 2, 2, 0, 0, 2, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(),
List.of(
new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse",
"NYEXX2609110214", null, null, 1),
new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse",
"NYEXX2609110215", null, null, 1)));
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE");
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
when(warehouses.activeWarehouse(Long.parseLong(warehouseId), "internal-test-token"))
.thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse, List.of())));
when(print.generateDispatchBol(eq("internal-test-token"), eq("request-2"), any()))
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
.header("X-Print-Page-Count", "2").body("%PDF-".getBytes(StandardCharsets.US_ASCII)));
service.generate(detail.dispatchNo(), "request-2");
ArgumentCaptor<PrintPdfClient.Request> request = ArgumentCaptor.forClass(PrintPdfClient.Request.class);
verify(print).generateDispatchBol(eq("internal-test-token"), eq("request-2"), request.capture());
assertThat(request.getValue().documents().stream()
.map(PrintPdfClient.Document::fbaWarehouseCode).toList())
.containsExactly("ABE2", "ONT8");
}
}
@@ -101,6 +101,7 @@ class DispatchServiceTest {
assertNotNull(DispatchService.class.getMethod("preArrange", String.class, String.class,
DispatchRequests.PreArrange.class).getAnnotation(Transactional.class));
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
when(items.findByDispatchOrder(eq("100"), any())).thenReturn(List.of(item(11L, "ONT8")));
var result = service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
null, null, null));
@@ -119,6 +120,22 @@ class DispatchServiceTest {
verify(items).insert(any(DispatchItem.class));
}
@Test
void preArrangeBuildsCommaSeparatedDistinctFbaRouteFromAssignedPallets() {
when(candidates.lockSelected("100", List.of(10L, 11L, 12L))).thenReturn(List.of(
candidate(10L, null, "ONT8"), candidate(11L, null, "ABE4"), candidate(12L, null, "ONT8")));
when(items.findByDispatchOrder(eq("100"), any())).thenReturn(List.of(
item(21L, "ONT8"), item(22L, "ABE4"), item(23L, "ONT8")));
DispatchResponse result = service.preArrange("100", "7",
new DispatchRequests.PreArrange(List.of(10L, 11L, 12L), null, null, null));
assertEquals("ONT8,ABE4", result.route());
ArgumentCaptor<DispatchOrder> dispatch = ArgumentCaptor.forClass(DispatchOrder.class);
verify(orders).insert(dispatch.capture());
assertEquals("ONT8,ABE4", dispatch.getValue().getRoute());
}
@Test
void movingPalletIntoDraftImmediatelyCreatesExpiringExclusiveLock() {
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
@@ -197,7 +214,7 @@ class DispatchServiceTest {
when(locks.lockActive("100", "PLAN:10")).thenReturn(lock);
when(drafts.lockActive("draft-1", "7")).thenReturn(draft);
service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, null, null, null, "draft-1"));
service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, null, null, "draft-1"));
assertNotNull(lock.getDispatchOrderId());
assertEquals("draft-1", lock.getDraftToken());
@@ -252,6 +269,17 @@ class DispatchServiceTest {
verify(locks).findActiveByDraftContainer("draft-1", 21L);
}
@Test
void draftContainerOnlyAcceptsFifteenOrThirtyPalletVehicles() {
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.createDraftContainer("7", new DispatchRequests.DraftContainer("draft-1", 14)));
assertEquals(409, exception.getStatusCode().value());
assertEquals("车辆大小仅支持15托或30托", exception.getReason());
verify(drafts, never()).lockActive(any(), any());
verify(draftContainers, never()).insert(any(DispatchDraftContainer.class));
}
@Test
void rejectsPalletAlreadyLockedByAnotherDispatch() {
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
@@ -315,13 +343,18 @@ class DispatchServiceTest {
dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1");
dispatch.setStatus(DispatchService.PENDING_DEPARTURE);
when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch);
DispatchRequests.Update request = new DispatchRequests.Update("ABC-1", "Driver", null, null, null,
DispatchItem ont8 = item(11L, "ONT8");
DispatchItem abe4 = item(12L, "ABE4");
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(ont8, abe4, item(13L, "ONT8")));
DispatchRequests.Update request = new DispatchRequests.Update("ABC-1", "Driver", null, null,
null, null, null, LocalDateTime.now());
DispatchResponse result = service.update("PC1", request);
assertEquals(DispatchService.PENDING_DEPARTURE, dispatch.getStatus());
assertEquals("ABC-1", result.vehiclePlate());
assertEquals("ONT8,ABE4", result.route());
assertEquals("ONT8,ABE4", dispatch.getRoute());
verify(orders).updateById(dispatch);
verify(items).findByDispatchOrder("100", 1L);
}
@@ -332,7 +365,7 @@ class DispatchServiceTest {
dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1");
dispatch.setStatus("DEPARTED");
when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch);
DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null, null,
DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null,
null, null, null, null);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
@@ -431,4 +464,11 @@ class DispatchServiceTest {
"LIP1-P001", destination, destination, destination, destination, packageCount,
new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED");
}
private DispatchItem item(long id, String fbaWarehouseCode) {
DispatchItem item = new DispatchItem();
item.setId(id);
item.setFbaWarehouseCode(fbaWarehouseCode);
return item;
}
}
@@ -24,8 +24,10 @@ class TailPlanCommandOpenApiContractTest {
int manualSchemaEnd = api.indexOf("\n PageMetadata:", manualSchemaStart);
assertTrue(manualSchemaStart >= 0 && manualSchemaEnd > manualSchemaStart);
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("inboundCode"));
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("plannedArrivalDate"));
assertFalse(java.util.Arrays.stream(ManualInboundPlanRequest.class.getRecordComponents())
.anyMatch(component -> "inboundCode".equals(component.getName())));
.anyMatch(component -> java.util.Set.of("inboundCode", "plannedArrivalDate")
.contains(component.getName())));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/hold:"));
assertFalse(api.contains("/api/platform/v1/last-leg/orders/{id}/detained:"));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/manual-nodes:"));
@@ -20,7 +20,6 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Set;
import java.time.LocalDateTime;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
@@ -106,6 +105,6 @@ class InboundPlanControllerTest {
private static ManualInboundPlanRequest manualRequest() {
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
LocalDate.of(2026, 9, 8), 2, "STANDARD", false, true, true, "加急入仓");
2, "STANDARD", false, true, true, "加急入仓");
}
}
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.pda.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerResponses;
import com.baiyue.lastleg.inboundplan.pda.gateway.DomesticLoadingEvidenceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.service.ReceivingContainerService;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
@@ -90,6 +91,21 @@ class ReceivingContainerControllerTest {
verify(service).downloadCurrentPhoto("42", "9999");
}
@Test
void downloadDomesticLoadingPhotoDelegatesWithoutExposingTheSourceFileId() {
when(service.downloadDomesticLoadingPhoto("42", "PHOTO_STEP_1")).thenReturn(
new DomesticLoadingEvidenceGateway.DownloadedFile(
"domestic".getBytes(), "empty-container.jpg", MediaType.IMAGE_JPEG_VALUE));
ResponseEntity<byte[]> response = controller.downloadDomesticLoadingPhoto(picker(), "PHOTO_STEP_1");
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
assertThat(response.getHeaders().getCacheControl()).isEqualTo("no-store");
assertThat(response.getBody()).containsExactly("domestic".getBytes());
verify(service).downloadDomesticLoadingPhoto("42", "PHOTO_STEP_1");
}
@Test
void pickerAuthorityWithoutWarehouseBindingIsForbidden() {
AuthenticatedUser unbound = new AuthenticatedUser(42L, 0L, 0L, 8L, "receiver", "PLATFORM", "PDA",
@@ -18,11 +18,13 @@ import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerOperation;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingFbaPalletCount;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.DomesticLoadingEvidenceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerOperationMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerSessionMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingFbaPalletCountMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
@@ -71,6 +73,8 @@ class ReceivingContainerServiceBehaviorTest {
@Mock DropshipOrderProfileMapper dropshipProfiles;
@Mock WarehouseResourceGateway warehouse;
@Mock UnloadPhotoFileGateway unloadPhotos;
@Mock DomesticLoadingEvidenceGateway domesticEvidenceFiles;
@Mock ContainerLoadingEvidenceMapper domesticEvidence;
@Mock InboundPdaWorkflowService workflow;
private ReceivingContainerService service;
@@ -93,11 +97,14 @@ class ReceivingContainerServiceBehaviorTest {
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
ReflectionTestUtils.setField(service, "warehouse", warehouse);
ReflectionTestUtils.setField(service, "unloadPhotos", unloadPhotos);
ReflectionTestUtils.setField(service, "domesticEvidenceFiles", domesticEvidenceFiles);
ReflectionTestUtils.setField(service, "domesticEvidence", domesticEvidence);
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);
org.mockito.Mockito.lenient().when(pallets.selectCount(any())).thenReturn(1L);
org.mockito.Mockito.lenient().when(domesticEvidence.listByContainer(any())).thenReturn(List.of());
}
@Test
@@ -846,6 +853,40 @@ class ReceivingContainerServiceBehaviorTest {
.containsExactly("SKU-A", "SKU-B", "SKU-C");
}
@Test
void mixedContainerFbaBulkNeverReceivesDropshipOrOrdinaryOverseasLines() {
InboundPlanLine fba = line("FBA");
InboundPlanLine dropship = line("FBA");
dropship.setId(20L); dropship.setOrderId(2L); dropship.setInboundCode("IN-DROPSHIP");
InboundPlanLine overseas = line("OVERSEAS");
overseas.setId(30L); overseas.setOrderId(3L); overseas.setInboundCode("IN-OVERSEAS");
LastlegOrder dropshipOrder = order("FBA");
dropshipOrder.setId(2L); dropshipOrder.setOrderType("ONE_CLICK_DROPSHIP");
LastlegOrder overseasOrder = order("OVERSEAS"); overseasOrder.setId(3L);
when(sessions.lockActiveByOperator("OPERATOR:7")).thenReturn(session("CONT-A", "SIGNED"));
when(plans.selectList(any())).thenReturn(List.of(plan("CONT-A")));
when(lines.selectList(any())).thenReturn(List.of(dropship, overseas, fba));
when(orders.selectById(1L)).thenReturn(order("FBA"));
when(orders.selectById(2L)).thenReturn(dropshipOrder);
when(orders.selectById(3L)).thenReturn(overseasOrder);
when(orderDetails.selectList(any())).thenReturn(List.of(detail("FBA-SKU", 2)));
stubReservedFbaPallet();
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
.thenAnswer(invocation -> invocation.getArgument(4));
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
var result = service.receiveBulk("7", "receiver",
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 2, "mixed-fba-receive"));
assertThat(result.acceptedPackages()).isEqualTo(2);
ArgumentCaptor<InboundPlanLine> captured = ArgumentCaptor.forClass(InboundPlanLine.class);
verify(workflow).receiveBulkResolved(any(), any(), captured.capture(), any(),
org.mockito.ArgumentMatchers.eq(2), any(), any());
assertThat(captured.getValue()).isSameAs(fba);
verifyNoInteractions(dropshipProfiles, dropshipCartons);
}
@Test
void dropshipCartonResolvesItsCompleteDpInsteadOfAPlanOrCustomer() {
stubCurrent("OVERSEAS", "CONT-A");
@@ -898,12 +939,36 @@ class ReceivingContainerServiceBehaviorTest {
assertThat(result.receivedPackages()).isEqualTo(2);
assertThat(result.pendingPackages()).isZero();
assertThat(result.palletCode()).isEqualTo("DP-260909-0001");
var duplicate = service.receiveWholePallet("7", "receiver",
new ReceivingContainerRequests.WholePalletReceive("DP-260909-0001", "whole-new-key"));
assertThat(duplicate.result()).isEqualTo("ALREADY_RECEIVED");
assertThat(duplicate.acceptedPackages()).isZero();
verify(sessions, org.mockito.Mockito.times(1)).updateById(any(ReceivingContainerSession.class));
verify(workflow).pickWholePallet(org.mockito.ArgumentMatchers.eq("100"),
org.mockito.ArgumentMatchers.eq("PLAN-1"), org.mockito.ArgumentMatchers.eq(30L),
any(com.baiyue.lastleg.inboundplan.pda.dto.PdaRequests.WholePalletOperation.class));
verifyNoInteractions(warehouse);
}
@Test
void domesticLoadingPhotoDownloadIsScopedToTheCurrentPhysicalContainer() {
ReceivingContainerSession current = session("CONT-A", "SIGNED");
current.setSourceContainerBookingId("91");
when(sessions.findActiveByOperator("OPERATOR:7")).thenReturn(current);
var evidence = new com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence();
evidence.setSourceContainerBookingId("91"); evidence.setEvidenceType("SEAL");
evidence.setSourceFileId(501L); evidence.setFileName("seal.jpg");
when(domesticEvidence.find("91", "SEAL")).thenReturn(evidence);
var downloaded = new DomesticLoadingEvidenceGateway.DownloadedFile(
new byte[]{1, 2}, "seal.jpg", "image/jpeg");
when(domesticEvidenceFiles.download(91L, 501L)).thenReturn(downloaded);
assertThat(service.downloadDomesticLoadingPhoto("7", "SEAL")).isSameAs(downloaded);
verify(domesticEvidenceFiles).download(91L, 501L);
assertThatThrownBy(() -> service.downloadDomesticLoadingPhoto("7", "UNKNOWN"))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("400");
}
private void stubCurrent(String destinationType, String containerNo) {
InboundPlan plan = plan(containerNo);
InboundPlanLine line = line(destinationType);
@@ -409,7 +409,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
}
private ManualInboundPlanRequest manualRequest() {
return new ManualInboundPlanRequest("100", 11L, 0, LocalDate.of(2026, 9, 10),
return new ManualInboundPlanRequest("100", 11L, 0,
1, "WOOD", false, false, false, "自动生成");
}
@@ -93,6 +93,10 @@ class InboundPlanServiceTest {
org.mockito.Mockito.lenient().when(orderMapper.lockContainerWarehouseFacts(any(), any())).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerWarehouseRef(
"100", 2L, "BOOKING-1", "193000000000200003")));
org.mockito.Mockito.lenient().when(orderMapper.findSourceContainerBookingId(any(), anyLong()))
.thenReturn("BOOKING-1");
org.mockito.Mockito.lenient().when(orderMapper.containerEstimatedWarehouseTimes(any()))
.thenReturn(List.of(LocalDateTime.of(2026, 9, 15, 8, 0)));
}
@Test
@@ -108,6 +112,7 @@ class InboundPlanServiceTest {
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
verify(planMapper).insert(plan.capture());
assertEquals(2L, plan.getValue().getSourceOrderId());
assertEquals(LocalDate.of(2026, 9, 15), plan.getValue().getPlannedArrivalDate());
assertEquals(1, plan.getValue().getLineCount());
assertEquals("PENDING_ASSIGNMENT", plan.getValue().getStatus());
assertEquals(1, plan.getValue().getPalletCount());
@@ -149,7 +154,7 @@ class InboundPlanServiceTest {
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any()))
.thenReturn(1);
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 2, "STANDARD", true, true, true, "优先收货");
2, "STANDARD", true, true, true, "优先收货");
var result = service.convertManual(request, "manual-2", "900");
@@ -167,6 +172,27 @@ class InboundPlanServiceTest {
verify(orderMapper).lockContainerWarehouseFacts("100", 2L);
}
@Test
void manualPlanRejectsMissingOrInconsistentContainerArrivalTime() {
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
2, "STANDARD", true, true, true, "优先收货");
when(orderMapper.containerEstimatedWarehouseTimes("BOOKING-1")).thenReturn(List.of());
ResponseStatusException missing = assertThrows(ResponseStatusException.class,
() -> service.convertManual(request, "manual-missing-arrival", "900"));
assertEquals(409, missing.getStatusCode().value());
assertEquals("来源柜预计到仓时间缺失,请先补齐柜级时间", missing.getReason());
when(orderMapper.containerEstimatedWarehouseTimes("BOOKING-1")).thenReturn(List.of(
LocalDateTime.of(2026, 9, 15, 8, 0), LocalDateTime.of(2026, 9, 16, 8, 0)));
ResponseStatusException inconsistent = assertThrows(ResponseStatusException.class,
() -> service.convertManual(request, "manual-inconsistent-arrival", "900"));
assertEquals(409, inconsistent.getStatusCode().value());
assertEquals("来源柜预计到仓时间不一致,请先修正柜级时间", inconsistent.getReason());
verify(orderMapper, never()).lockOrder(any(), any());
verify(planMapper, never()).insert(any(InboundPlan.class));
}
@Test
void manualPlanMayExceedButCannotUndercutPreviewPalletCount() {
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
@@ -175,7 +201,7 @@ class InboundPlanServiceTest {
null, null, "Amazon ACY2", 0, 0, false, 0);
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
2, "WOOD", false, false, false, null), "manual-below-preview", "900"));
assertEquals(400, error.getStatusCode().value());
@@ -192,7 +218,7 @@ class InboundPlanServiceTest {
new DropshipPalletSnapshot("DP-260914-0001", 10L, List.of())));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
2, "WOOD", false, false, false, null), "dropship-pallet-mismatch", "900"));
assertEquals(400, error.getStatusCode().value());
@@ -212,7 +238,7 @@ class InboundPlanServiceTest {
.thenReturn(1);
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 1, "WOOD", false, false, false, null),
1, "WOOD", false, false, false, null),
"dropship-exact-pallet-count", "900");
assertEquals(1, result.palletCount());
@@ -266,7 +292,7 @@ class InboundPlanServiceTest {
.thenReturn(1);
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 3, "STANDARD", true, false, false, null),
3, "STANDARD", true, false, false, null),
"manual-id-only", "900");
assertEquals("US-LAX-01", result.overseasWarehouseCode());
@@ -282,7 +308,7 @@ class InboundPlanServiceTest {
new InboundPlanOrderMapper.ContainerWarehouseRef("200", 3L, "BOOKING-1", "193000000000200004")));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
1, "STANDARD", false, false, false, null), "mixed-warehouse", "900"));
assertEquals(409, error.getStatusCode().value());
@@ -300,7 +326,7 @@ class InboundPlanServiceTest {
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 5, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 5,
1, "STANDARD", false, false, false, null),
"manual-held", "900"));
@@ -1,6 +1,7 @@
package com.baiyue.lastleg.looseboard.service;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOperation;
@@ -85,6 +86,7 @@ class LocalLooseboardServiceTest {
assertEquals("100", detail.summary().customerId());
assertEquals(LocalLooseboardService.PENDING_ACCEPTANCE, detail.summary().status());
assertEquals("SHARED_TRUCK", detail.summary().transportMode());
assertEquals("40HQ", detail.summary().containerType());
verify(lastlegOrders).insert(org.mockito.ArgumentMatchers.<LastlegOrder>argThat(value ->
"LOCAL_LOOSEBOARD".equals(value.getOrderType())
&& value.getSourceOrderNo().equals("LB-WH-20260901-000001")
@@ -94,6 +96,22 @@ class LocalLooseboardServiceTest {
"CREATE".equals(value.getOperationType())));
}
@Test
void createDefaultsTheContainerTypeAndRejectsRemovedTypes() {
AtomicReference<LocalLooseboardOrder> saved = new AtomicReference<>();
when(orders.insert(any(LocalLooseboardOrder.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("default-type", null));
assertEquals("40HQ", detail.summary().containerType());
assertThrows(BizException.class, () -> service.create("100", "7", create("retired-type", "40HC")));
}
@Test
void ordinaryUpdateCannotChangeAcceptedOrder() {
LocalLooseboardOrder order = order(LocalLooseboardService.DISPATCHED, 0L);
@@ -174,8 +192,12 @@ class LocalLooseboardServiceTest {
}
private LocalLooseboardRequests.Create create(String key) {
return create(key, "40HQ");
}
private LocalLooseboardRequests.Create create(String key, String containerType) {
return new LocalLooseboardRequests.Create("SHARED_TRUCK", "YES", "WAREHOUSE_TO_WAREHOUSE",
"Los Angeles", "Address A", "Ontario", "Address B", "40HQ", 12,
"Los Angeles", "Address A", "Ontario", "Address B", containerType, 12,
new BigDecimal("100.000"), new BigDecimal("2.500000"), new BigDecimal("500.00"),
"USD", "Alice", "+1-555-0100", LocalDate.now().plusDays(2), null, key);
}
@@ -28,11 +28,13 @@ class RelabelDocumentServiceTest {
when(applications.lockNo("101","RL-1")).thenReturn(application);when(applications.findNo("101","RL-1")).thenReturn(application);
}
@Test void totalUploadPersistsReturnedMetadataAndExposesSummary(){
when(files.upload("101","OVERSEAS_RELABEL_TOTAL_DOCUMENT","11",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(99L,"labels.pdf","application/pdf",15));
when(files.upload("101","OVERSEAS_RELABEL_CARTON_TOTAL_DOCUMENT","11",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(99L,"labels.pdf","application/pdf",15));
var result=service.uploadTotalFile("101","RL-1",7L,pdf);
assertThat(result.application().totalFileId()).isEqualTo("99");
assertThat(result.application().totalFileName()).isEqualTo("labels.pdf");
assertThat(result.application().totalFileUrl()).endsWith("/files/99/download?scope=TOTAL");
assertThat(application.getCartonTotalFileId()).isEqualTo("99");
verify(files).upload("101","OVERSEAS_RELABEL_CARTON_TOTAL_DOCUMENT","11",7L,pdf);
verify(applications).updateById(application);
}
@Test void itemUploadBindsOwnCustomerApplicationAndDistinctCategory(){
@@ -14,6 +14,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class Phase5AdminPermissionContractTest {
@@ -34,14 +35,14 @@ class Phase5AdminPermissionContractTest {
}
@Test
void customerVisitAndDeliveryStatusEndpointsUseIndependentAuthorities() {
void customerVisitAndEditableDeliveryStatusEndpointsUseIndependentAuthorities() {
Map<String, String> permissions = permissions(CustomerVisitController.class);
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_LIST), permissions.get("page"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_LIST), permissions.get("detail"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_EDIT), permissions.get("save"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_EDIT), permissions.get("upload"));
assertEquals(authority(OverseasPermissions.DELIVERY_STATUS), permissions.get("completeDelivery"));
assertFalse(permissions.containsKey("completeDelivery"));
assertEquals(authority(OverseasPermissions.DELIVERY_STATUS), permissions.get("updateDeliveryStatus"));
}
@@ -76,6 +77,7 @@ class Phase5AdminPermissionContractTest {
}) {
assertTrue(yaml.contains(path), path);
}
assertFalse(yaml.contains("/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/complete:"));
}
private static Map<String, String> permissions(Class<?> controller) {
@@ -3,8 +3,8 @@ import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
class LoadingCompletedConsumerTest {
private final Map<String,String> frozenContainerNos=new HashMap<>();
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock LastlegOrderNoDailySequenceMapper sequences; 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,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); 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(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(2); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper loadingEvidence; @Mock LastlegOrderNoDailySequenceMapper sequences; 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,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"loadingEvidence",loadingEvidence); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); 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(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(2); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); 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.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&"193000000000200003".equals(o.getOverseasWarehouseId())&&"US-LAX-01".equals(o.getOverseasWarehouseCode())&&"洛杉矶仓".equals(o.getOverseasWarehouseName())&&Integer.valueOf(0).equals(o.getReceivingWarehouseVersion()))); 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()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
@Test void oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); com.fasterxml.jackson.databind.node.ArrayNode rows=json.createArrayNode(); for(int index=1;index<=15;index++){ com.fasterxml.jackson.databind.node.ObjectNode row=node.path("orders").get(0).deepCopy(); row.put("sourceInboundId","in-"+index).put("rootInboundId","root-"+index).put("sourceOrderId","order-"+index).put("sourceOrderNo","FL-"+index).put("rootOrderId","root-order-"+index).put("inboundCode","RK-"+index); rows.add(row); } node.set("orders",rows); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope fifteen=new EventEnvelope("123e4567-e89b-12d3-a456-426614174015",source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(15,service.consume(fifteen)); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(15)).insert(captured.capture()); assertEquals(1,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(captured.getAllValues().get(0).getOrderNo().matches("^FL-WH-\\d{8}-\\d{6}$")); verify(sequences,never()).nextValue(any(java.time.LocalDate.class)); }
@Test void rejectsMissingOwnerSnapshotBeforeAnyLastlegWrite(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node.path("orders").get(0)).put("customerName"," "); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope missing=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(missing)); assertTrue(error.getMessage().contains("尾程事件负载无效")||error.getMessage().contains("货主快照")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
@@ -241,6 +241,26 @@ class LoadingCompletedConsumerTest {
return new EventEnvelope(original.eventId(),original.eventType(),2,original.occurredAt(),original.sourceRegion(),original.targetRegion(),original.tenantId(),original.aggregateType(),original.aggregateId(),original.aggregateVersion(),2L,original.traceId(),original.correlationId(),original.causationId(),node,hash);
}
private EventEnvelope event(){ var item1=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-1","产品","Product",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("12"),new BigDecimal("1.2"),"wh-1","ONT8","FBA ONT8","FBA-1","SHIP-1","MARK-1"); var item2=new FirstLegLoadingPlanCompletedV1.Item("d-2","SKU-2","产品2","Product2",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("9"),new BigDecimal("0.8"),"wh-2","LAX9","FBA LAX9","FBA-2","SHIP-2","MARK-2"); var p=new FirstLegLoadingPlanCompletedV1("plan-1","LP1","book-1","MSCU6639870","CAB1","ow-1","洛杉矶海外仓",List.of(new FirstLegLoadingPlanCompletedV1.Order("in-1","root-1","ord-1","FL1","root-o1","RK1","wh-1","ONT8","FBA ONT8",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("1.2"),List.of(item1),"100","Customer 100","FIRST_LEG",List.of()),new FirstLegLoadingPlanCompletedV1.Order("in-2","root-2","ord-2","FL2","root-o2","RK2","wh-2","LAX9","FBA LAX9",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("0.8"),List.of(item2),"100","Customer 100","FIRST_LEG",List.of()))); JsonNode node=json.valueToTree(p); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174000",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-1",2,1L,"123e4567-e89b-12d3-a456-426614174001","FIRSTLEG_LOADING_COMPLETED:plan-1:100:1",null,node,hash); }
@Test void projectsFiveDomesticLoadingPhotosOncePerPhysicalContainer(){
EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=(com.fasterxml.jackson.databind.node.ObjectNode)v2Payload(source);
var values=node.putArray("loadingEvidence"); int fileId=10;
for(String type:List.of("PHOTO_STEP_1","PHOTO_STEP_2","PHOTO_STEP_3","PHOTO_STEP_4","SEAL")) values.addObject().put("evidenceType",type).put("fileId",fileId++).put("fileName",type+".jpg");
String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
EventEnvelope event=new EventEnvelope(source.eventId(),source.eventType(),3,source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),3L,source.traceId(),source.correlationId(),source.causationId(),node,hash);
assertEquals(2,service.consume(event));
ArgumentCaptor<com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence> captured=ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence.class);
verify(loadingEvidence,times(5)).insert(captured.capture());
assertEquals(Set.of("PHOTO_STEP_1","PHOTO_STEP_2","PHOTO_STEP_3","PHOTO_STEP_4","SEAL"),captured.getAllValues().stream().map(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence::getEvidenceType).collect(java.util.stream.Collectors.toSet()));
assertTrue(captured.getAllValues().stream().allMatch(value->"book-1".equals(value.getSourceContainerBookingId())));
}
@Test void rejectsV3EventWithoutTheFiveDomesticLoadingPhotos(){
EventEnvelope source=event(); JsonNode node=v2Payload(source);
String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
EventEnvelope event=new EventEnvelope(source.eventId(),source.eventType(),3,source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),3L,source.traceId(),source.correlationId(),source.causationId(),node,hash);
IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(event));
assertTrue(error.getMessage().contains("五类国内装柜照片"));
verify(loadingEvidence,never()).insert(any(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence.class));
}
private JsonNode v2Payload(EventEnvelope source){ com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); node.set("sourceContainerBookingId",node.remove("containerBookingId")); return node; }
private EventEnvelope eventForPayload(FirstLegLoadingPlanCompletedV1 payload, long version){ JsonNode node=json.valueToTree(payload); for(JsonNode order:node.path("orders")) for(JsonNode pallet:order.path("pallets")) addManifest(pallet,order.path("packageCount").asInt()); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174099",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN",payload.loadingPlanId(),2,version,"123e4567-e89b-12d3-a456-426614174098","FIRSTLEG_LOADING_COMPLETED:"+payload.loadingPlanId()+":100:1",null,node,hash); }
private void addManifest(JsonNode pallet,int packageCount){
@@ -43,7 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
class TaillegRealConsumerAcceptanceTest {
static final String CUSTOMER = "990022";
static final String MARKER = "REAL-TAIL-20260822-R3";
static final String EVENT_KEY = "FIRSTLEG_LOADING_COMPLETED:990022022:990022:2";
static final String EVENT_KEY = "FIRSTLEG_LOADING_COMPLETED:990022022:990022:3";
static final LocalDateTime RELABEL_DEADLINE = LocalDateTime.of(2026, 8, 29, 11, 1, 21, 628_000_000);
static final LocalDateTime TRANSFER_ETA = LocalDateTime.of(2026, 9, 1, 11, 1, 21, 674_000_000);
static final LocalDateTime RETURN_RELABEL_ETA = LocalDateTime.of(2026, 9, 1, 11, 1, 21, 756_000_000);
@@ -62,7 +62,7 @@ class TaillegRealConsumerAcceptanceTest {
InboundPlanService.class, InventoryCommandService.class, RelabelApplicationService.class,
WarehouseTransferService.class, LocalLooseboardService.class})
@MapperScan({"com.baiyue.lastleg.mapper", "com.baiyue.lastleg.dropship.mapper",
"com.baiyue.lastleg.inboundplan.mapper", "com.baiyue.lastleg.inventory.mapper",
"com.baiyue.lastleg.inboundplan.mapper", "com.baiyue.lastleg.inboundplan.pda.mapper", "com.baiyue.lastleg.inventory.mapper",
"com.baiyue.lastleg.inventory.command.mapper", "com.baiyue.lastleg.relabel.mapper",
"com.baiyue.lastleg.transfer.mapper", "com.baiyue.lastleg.looseboard.mapper"})
static class AcceptanceApplication {
@@ -1,5 +1,7 @@
package com.baiyue.lastleg.trucking.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
import com.baiyue.lastleg.trucking.entity.UsTruckOrderOperation;
@@ -18,6 +20,8 @@ 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;
@@ -31,6 +35,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class)
class UsTruckOrderServiceTest {
@@ -38,6 +43,7 @@ class UsTruckOrderServiceTest {
@Mock UsTruckOrderPalletMapper pallets;
@Mock UsTruckOrderStatusHistoryMapper histories;
@Mock UsTruckOrderOperationMapper operations;
@Mock LastlegOrderNoGenerator orderNoGenerator;
private final List<UsTruckOrderPallet> palletRows = new ArrayList<>();
private final List<UsTruckOrderStatusHistory> historyRows = new ArrayList<>();
private final Map<String, UsTruckOrderOperation> operationRows = new HashMap<>();
@@ -52,20 +58,21 @@ class UsTruckOrderServiceTest {
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
order = order(UsTruckOrderService.PENDING_REVIEW, 0L, 2);
when(orders.lockNo("100", "TK1")).thenAnswer(call -> order);
lenient().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(pallets.listOrder(anyString(), anyLong())).thenAnswer(call -> palletRows);
lenient().when(histories.listOrder("100", 1L)).thenAnswer(call -> historyRows);
when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
historyRows.add(call.getArgument(0)); return 1;
});
when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
lenient().when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
UsTruckOrderPallet row = call.getArgument(0); palletRows.add(row); return 1;
});
when(operations.findKey(anyString(), anyLong(), anyString()))
lenient().when(operations.findKey(anyString(), anyLong(), anyString()))
.thenAnswer(call -> operationRows.get(call.getArgument(2)));
when(operations.insert(any(UsTruckOrderOperation.class))).thenAnswer(call -> {
lenient().when(operations.insert(any(UsTruckOrderOperation.class))).thenAnswer(call -> {
UsTruckOrderOperation row = call.getArgument(0); operationRows.put(row.getIdempotencyKey(), row); return 1;
});
UsTruckOrderStatusHistory created = new UsTruckOrderStatusHistory();
@@ -73,6 +80,21 @@ class UsTruckOrderServiceTest {
created.setToStatus(UsTruckOrderService.PENDING_REVIEW); historyRows.add(created);
}
@Test
void createDefaultsTheContainerTypeAndRejectsRemovedTypes() {
when(orderNoGenerator.next(anyString(), anyString(), anyLong())).thenReturn("TK1");
when(histories.listOrder(anyString(), anyLong())).thenReturn(historyRows);
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> {
order = call.getArgument(0);
return 1;
});
var detail = service.create("100", "operator", create(null, "default-type"));
assertEquals("40HQ", detail.summary().containerType());
assertThrows(BizException.class, () -> service.create("100", "operator", create("40HC", "retired-type")));
}
@Test
void completeLifecycleUsesFactsIdempotencyOperatorsAndCannotReverseTerminal() {
var review = new UsTruckOrderRequests.Review(0L, "APPROVE", "review-1", "approve");
@@ -165,6 +187,12 @@ class UsTruckOrderServiceTest {
return new UsTruckOrderRequests.Command(version, key, key);
}
private UsTruckOrderRequests.Create create(String containerType, String key) {
return new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null, "Ontario", null,
containerType, 1, 0, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "Alice",
"+1-555-0100", LocalDate.now().plusDays(1), null, key);
}
private UsTruckOrder order(String status, long version, int pallets) {
UsTruckOrder value = new UsTruckOrder();
value.setId(1L); value.setCustomerId("100"); value.setOrderNo("TK1");