优化
This commit is contained in:
+7
-1
@@ -1,7 +1,10 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
@@ -12,6 +15,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -24,7 +28,9 @@ class LastlegOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void customerCannotOverrideItsTenantScope() {
|
||||
controller.list(user(100L, "MERCHANT"), "200", query);
|
||||
ApiResponse<PageResult<LastlegOrderSummary>> response =
|
||||
controller.list(user(100L, "MERCHANT"), "200", query);
|
||||
assertNotNull(response);
|
||||
verify(service).page(eq("100"), eq(query));
|
||||
}
|
||||
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.lastleg.delivery.controller.DeliveryTransferController;
|
||||
import com.baiyue.lastleg.dispatch.controller.DispatchController;
|
||||
import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController;
|
||||
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPalletAssignmentController;
|
||||
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPdaController;
|
||||
import com.baiyue.lastleg.inventory.command.controller.InventoryCommandController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class Phase4BrowserApiBoundaryTest {
|
||||
@Test
|
||||
void inboundPdaAndUnloadPhotoApisUseThePdaAccessDomain() {
|
||||
assertTrue(basePath(InboundPdaController.class).startsWith("/api/pda/v1/"));
|
||||
assertTrue(basePath(InboundPalletAssignmentController.class).startsWith("/api/pda/v1/"));
|
||||
assertFalse(basePath(InboundPdaController.class).contains("/api/platform/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludedOutboundControllersAreDisabledUnlessExplicitlyEnabledLater() {
|
||||
for (Class<?> type : new Class<?>[] {
|
||||
DispatchController.class, DeliveryTransferController.class, DispatchPdaController.class
|
||||
}) {
|
||||
ConditionalOnProperty condition = type.getAnnotation(ConditionalOnProperty.class);
|
||||
assertTrue(condition != null, type.getSimpleName());
|
||||
assertTrue(Arrays.asList(condition.name()).contains("lastleg.future-outbound-api.enabled"));
|
||||
assertFalse(condition.matchIfMissing());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformInventoryCommandsDoNotMapOutboundAllocationPickingOrShipping() {
|
||||
Set<String> paths = Arrays.stream(InventoryCommandController.class.getDeclaredMethods())
|
||||
.flatMap(method -> Arrays.stream(method.getAnnotations()))
|
||||
.filter(annotation -> annotation.annotationType().getName().startsWith("org.springframework.web.bind.annotation"))
|
||||
.flatMap(annotation -> {
|
||||
try {
|
||||
return Arrays.stream((String[]) annotation.annotationType().getMethod("value").invoke(annotation));
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
return java.util.stream.Stream.empty();
|
||||
}
|
||||
}).collect(Collectors.toSet());
|
||||
assertFalse(paths.contains("/allocate"));
|
||||
assertFalse(paths.contains("/unallocate"));
|
||||
assertFalse(paths.contains("/pick"));
|
||||
assertFalse(paths.contains("/ship"));
|
||||
}
|
||||
|
||||
private static String basePath(Class<?> type) {
|
||||
return type.getAnnotation(RequestMapping.class).value()[0];
|
||||
}
|
||||
}
|
||||
+6
-19
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryResponses;
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
|
||||
@@ -204,32 +203,20 @@ class DeliveryTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundDetailReturnsPagedPalletDestinationPackagesAndInboundCode() {
|
||||
void outboundDetailDoesNotExposeExcludedPalletOrOutboundFulfillmentRows() {
|
||||
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "transfer-101");
|
||||
DeliveryOrder delivery = delivery(401L, outbound, "DEL-401");
|
||||
OutboundPalletItem first = pallet(501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12);
|
||||
OutboundPalletItem second = pallet(502L, "PLT-502", "LAX9", "Los Angeles", "IN-B", 8);
|
||||
when(outboundOrders.findByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
|
||||
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
|
||||
when(outboundPallets.selectPage(any(Page.class), any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked") Page<OutboundPalletItem> page = invocation.getArgument(0);
|
||||
page.setRecords(List.of(first, second));
|
||||
page.setTotal(2);
|
||||
return page;
|
||||
});
|
||||
|
||||
DeliveryResponses.OutboundDetail detail = service.outboundDetail("TENANT-A", "OUT-301",
|
||||
new DeliveryRequests.PalletQuery(1, 20, null));
|
||||
|
||||
PageResult<DeliveryResponses.PalletRow> pallets = detail.pallets();
|
||||
assertEquals(2, pallets.total());
|
||||
assertEquals(2, pallets.records().size());
|
||||
assertEquals("ONT8", pallets.records().get(0).destinationWarehouseCode());
|
||||
assertEquals("Ontario", pallets.records().get(0).destinationWarehouseName());
|
||||
assertEquals(12, pallets.records().get(0).packageCount());
|
||||
assertEquals("IN-A", pallets.records().get(0).inboundCode());
|
||||
assertEquals("LAX9", pallets.records().get(1).destinationWarehouseCode());
|
||||
assertEquals(8, pallets.records().get(1).packageCount());
|
||||
assertEquals("OUT-301", detail.outbound().outboundNo());
|
||||
assertEquals("DEL-401", detail.delivery().deliveryNo());
|
||||
assertTrue(java.util.Arrays.stream(DeliveryResponses.OutboundDetail.class.getRecordComponents())
|
||||
.noneMatch(component -> "pallets".equals(component.getName())));
|
||||
verify(outboundPallets, never()).selectPage(any(Page.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-2
@@ -10,11 +10,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanSchemaContractTest {
|
||||
@Test
|
||||
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
|
||||
void planIsUniqueByCustomerAndSourceOrderWhileContainerCanBeShared() throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||
assertTrue(stream != null, "lastleg schema must be available");
|
||||
String schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_source_order (customer_id, source_order_id)"));
|
||||
assertTrue(schema.contains("key idx_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertFalse(schema.contains("unique key uk_ll_inbound_plan_container"));
|
||||
assertTrue(schema.contains("default 'pending_assignment'"));
|
||||
assertTrue(schema.contains("required_package_count"));
|
||||
assertTrue(schema.contains("planned_package_count"));
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.inboundplan.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class InboundPlanPermissionContractTest {
|
||||
private static final String LIST = "platform:overseas-warehouse:inbound-order:list";
|
||||
|
||||
@Test
|
||||
void listAndDetailUseOnlyCanonicalInboundOrderListAuthority() {
|
||||
Map<String, Method> methods = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
|
||||
.collect(Collectors.toMap(Method::getName, method -> method));
|
||||
|
||||
for (String name : new String[] {"page", "detail", "fbaAppointmentDetail"}) {
|
||||
Method method = methods.get(name);
|
||||
assertEquals("hasAuthority('" + LIST + "')", method.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("Required authority: " + LIST, method.getAnnotation(Operation.class).description());
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -13,6 +13,7 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
@@ -42,6 +43,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundStockReceiptServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper orderDetails;
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@@ -52,6 +54,7 @@ class InboundStockReceiptServiceTest {
|
||||
void setUp() {
|
||||
service = new InboundStockReceiptService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
@@ -74,6 +77,7 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals(5L, request.getValue().packages());
|
||||
assertEquals(101L, request.getValue().inboundOrderId());
|
||||
assertEquals("IN-1", request.getValue().inboundCode());
|
||||
assertEquals("SKU-1", request.getValue().skuCode());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
@@ -84,10 +88,11 @@ class InboundStockReceiptServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void storesOneInboundOrderAcrossMultiplePalletsWithoutSkuSplitting() {
|
||||
void splitsOneInboundOrderAcrossSkuAndPalletDimensions() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
fixture.detail.setPackageCount(5);
|
||||
LastlegOrderDetail second = detail(202L, "SKU-2", 5, 7);
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(fixture.detail, second));
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
@@ -187,6 +192,7 @@ class InboundStockReceiptServiceTest {
|
||||
order.setCurrentStatus("INBOUND_PLANNED");
|
||||
org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order);
|
||||
LastlegOrderDetail detail = detail(201L, "SKU-1", required, quantity);
|
||||
org.mockito.Mockito.lenient().when(orderDetails.selectList(any())).thenReturn(List.of(detail));
|
||||
return new Fixture(plan, task, line, order, detail, allocation(31L, 12L, 21L, picked), pallet(21L, 2001L));
|
||||
}
|
||||
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
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.OverseasContainerUnloadedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.client.EventBridgeClient;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundUnloadCompletedOutboxRelayTest {
|
||||
@Mock private UnloadCompletedOutboxRepository repository;
|
||||
@Mock private EventBridgeClient client;
|
||||
private ObjectMapper objectMapper;
|
||||
private InboundUnloadCompletedOutboxRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
relay = new InboundUnloadCompletedOutboxRelay();
|
||||
ReflectionTestUtils.setField(relay, "repository", repository);
|
||||
ReflectionTestUtils.setField(relay, "client", client);
|
||||
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
|
||||
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksOutboxPublishedAfterDomesticBridgeAcknowledgesSameEvent() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
LastlegEventOutbox row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
|
||||
.thenReturn(ApiResponse.success(new EventBridgeClient.Acknowledgement(
|
||||
event.eventId(), "已接收", "2026-08-20T03:01:00")));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).published(eq(9001L), anyString(), any(LocalDateTime.class));
|
||||
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesLeaseForRetryWhenBridgeDeliveryFails() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
LastlegEventOutbox row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
|
||||
.thenThrow(new IllegalStateException("bridge unavailable"));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("bridge unavailable"));
|
||||
verify(repository, never()).published(anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
private LastlegEventOutbox row(EventEnvelope event) throws Exception {
|
||||
LastlegEventOutbox row = new LastlegEventOutbox();
|
||||
row.setId(9001L);
|
||||
row.setEventKey("OverseasContainerUnloaded:7001");
|
||||
row.setPayloadJson(objectMapper.writeValueAsString(event));
|
||||
return row;
|
||||
}
|
||||
|
||||
private EventEnvelope event() {
|
||||
var payload = objectMapper.valueToTree(new OverseasContainerUnloadedV1("1001", "MSCU1234567",
|
||||
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null));
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000009",
|
||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1, Instant.parse("2026-08-20T03:00:01Z"),
|
||||
RegionCode.OVERSEAS, RegionCode.CN, "1", "CONTAINER_BOOKING", "1001", 7L, 1L,
|
||||
"trace-12345678", "correlation-12345678", null, payload,
|
||||
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
|
||||
}
|
||||
}
|
||||
+32
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -115,6 +118,35 @@ class InboundPlanServiceTest {
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesInboundPlansWithinCustomerScope() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(
|
||||
2, 20, "LIP", "RK", "PENDING_ASSIGNMENT", "WH-1", "ONT8", "ONT8",
|
||||
"NOT_APPOINTED", null, null);
|
||||
InboundPlanSummary row = new InboundPlanSummary(1L, "LIP1", 2L, "100", "Customer A",
|
||||
"FBA", "RK-1", "CONT-1", "WH-1", "LA Warehouse", "ONT8", "ONT8", "ONT8",
|
||||
"ONT8", 10, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", "NOT_APPOINTED", null, null);
|
||||
when(planMapper.countRows("100", query)).thenReturn(21L);
|
||||
when(planMapper.pageRows("100", query, 20L, 20L)).thenReturn(List.of(row));
|
||||
|
||||
var result = service.page("100", query);
|
||||
|
||||
assertEquals(21L, result.total());
|
||||
assertEquals(2L, result.pages());
|
||||
assertEquals("RK-1", result.records().get(0).inboundCode());
|
||||
verify(planMapper).pageRows("100", query, 20L, 20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReversedInboundPlanDateRange() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null, null,
|
||||
null, null, null, LocalDateTime.of(2026, 8, 20, 0, 0),
|
||||
LocalDateTime.of(2026, 8, 19, 0, 0));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.page("100", query));
|
||||
verify(planMapper, never()).countRows(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
|
||||
+6
-4
@@ -28,12 +28,13 @@ class InventorySchemaContractTest {
|
||||
}
|
||||
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_balance"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_dimension (customer_id,overseas_warehouse_id,destination_warehouse_code,inbound_order_id,inbound_code)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_dimension (customer_id,overseas_warehouse_id,destination_warehouse_code,inbound_order_id,inbound_code,sku_code)"));
|
||||
assertTrue(schema.contains("inbound_order_id bigint not null"));
|
||||
assertTrue(schema.contains("inbound_code varchar(128) not null"));
|
||||
assertTrue(schema.contains("available_packages bigint not null default 0"));
|
||||
assertTrue(schema.contains("in_transit_packages bigint not null default 0"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_balance").contains("sku_code"));
|
||||
assertTrue(tableDefinition(schema, "ll_inventory_balance").contains("sku_code"));
|
||||
assertTrue(tableDefinition(schema, "ll_inventory_balance").contains("product_name_zh"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_balance").contains("_quantity"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_position"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_position (balance_id,pallet_id,batch_no)"));
|
||||
@@ -52,7 +53,7 @@ class InventorySchemaContractTest {
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_ledger_idempotency (customer_id,idempotency_key)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_operation"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_stock_receipt"));
|
||||
assertTrue(schema.contains("unique key uk_ll_stock_receipt_task_allocation (customer_id,task_id,allocation_id)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_stock_receipt_task_allocation_sku (customer_id,task_id,allocation_id,sku_code)"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_ledger").contains("updated_at"));
|
||||
}
|
||||
|
||||
@@ -97,7 +98,8 @@ class InventorySchemaContractTest {
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("inboundOrderId"));
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("inboundCode"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("inboundOrderId"));
|
||||
assertThrowsNoField(InventoryBalance.class, "skuCode");
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("skuCode"));
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("productNameZh"));
|
||||
assertThrowsNoField(InventoryBalance.class, "availableQuantity");
|
||||
assertThrowsNoField(InventoryPosition.class, "skuCode");
|
||||
assertThrowsNoField(InventoryPosition.class, "productQuantity");
|
||||
|
||||
+6
-4
@@ -63,7 +63,7 @@ class InventoryCommandServiceTest {
|
||||
void stockInUpdatesBalancePositionAndLedgerInOneTransaction() throws Exception {
|
||||
assertTrue(InventoryCommandService.class.getMethod("stockIn", String.class, String.class,
|
||||
InventoryCommandRequests.StockIn.class).getAnnotation(Transactional.class) != null);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(null);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(null);
|
||||
var result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-1", 12, 24));
|
||||
|
||||
assertEquals(12, result.availablePackages());
|
||||
@@ -71,6 +71,7 @@ class InventoryCommandServiceTest {
|
||||
ArgumentCaptor<InventoryBalance> balance = ArgumentCaptor.forClass(InventoryBalance.class);
|
||||
verify(balances).updateById(balance.capture());
|
||||
assertEquals(12L, balance.getValue().getAvailablePackages());
|
||||
assertEquals("SKU-1", balance.getValue().getSkuCode());
|
||||
verify(positions).insert(any(InventoryPosition.class));
|
||||
verify(ledgers).insert(any(InventoryLedger.class));
|
||||
verify(operations).updateById(any(InventoryOperation.class));
|
||||
@@ -78,7 +79,7 @@ class InventoryCommandServiceTest {
|
||||
|
||||
@Test
|
||||
void stockInAllowsPalletPositionWithZeroOptionalPackages() {
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(null);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(null);
|
||||
|
||||
var result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-zero", 0, 0));
|
||||
|
||||
@@ -104,7 +105,7 @@ class InventoryCommandServiceTest {
|
||||
@Test
|
||||
void replayUsesPersistedResultWithoutMutatingInventoryAgain() throws Exception {
|
||||
var request = stockIn("IN-1", "stock-in-1", 12, 24);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(null);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(null);
|
||||
service.stockIn("100", "7", request);
|
||||
ArgumentCaptor<InventoryOperation> operation = ArgumentCaptor.forClass(InventoryOperation.class);
|
||||
verify(operations).insert(operation.capture());
|
||||
@@ -138,7 +139,8 @@ class InventoryCommandServiceTest {
|
||||
|
||||
private InventoryCommandRequests.StockIn stockIn(String inbound, String key, long packages, long quantity) {
|
||||
return new InventoryCommandRequests.StockIn("100", "Customer 100", "WH-1", "WH1", "Warehouse 1",
|
||||
"DEST-1", "ONT8", "ONT8", 101L, inbound, "IN-CODE-1", 300L, "PLT-1", null, null, null,
|
||||
"DEST-1", "ONT8", "ONT8", 101L, inbound, "IN-CODE-1", "SKU-1", "Product 1", null,
|
||||
300L, "PLT-1", null, null, null,
|
||||
packages, "ORD-1", "EVT-1", key, null);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class InventoryQueryControllerTest {
|
||||
private final InventoryQueryService service = mock(InventoryQueryService.class);
|
||||
private final InventoryQueryController controller = controller();
|
||||
private final InventoryQueryRequests.BalanceQuery query = new InventoryQueryRequests.BalanceQuery(
|
||||
1, 20, null, null, null, null, null, null, null, null);
|
||||
1, 20, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantCannotOverrideCustomerScope() {
|
||||
|
||||
+2
-3
@@ -120,12 +120,11 @@ class InventoryQueryServiceTest {
|
||||
|
||||
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,totalPackages"));
|
||||
assertFalse(csv.contains("skuCode"));
|
||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages"));
|
||||
assertFalse(csv.contains("Quantity"));
|
||||
}
|
||||
|
||||
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null, status);
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null, null, null, status);
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.looseboard;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LocalLooseboardSchemaContractTest {
|
||||
@Test
|
||||
void baselineContainsIndependentLooseboardOrderAndHistory() throws Exception {
|
||||
String sql;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
sql = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
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("operation_type VARCHAR(32) NOT NULL"));
|
||||
assertTrue(sql.contains("uk_ll_local_looseboard_no(customer_id,order_no)"));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.lastleg.looseboard.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
|
||||
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
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.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PlatformLocalLooseboardControllerTest {
|
||||
@Mock LocalLooseboardService service;
|
||||
private PlatformLocalLooseboardController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new PlatformLocalLooseboardController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUsesExplicitCustomerAndOperator() {
|
||||
var request = create();
|
||||
controller.create(user(Set.of(OverseasPermissions.LOOSEBOARD_CREATE)), "100", request);
|
||||
verify(service).create("100", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRequiresIndependentEditPermission() {
|
||||
var request = new LocalLooseboardRequests.Update(0L, "SHARED_TRUCK", "NO", "PORT_PICKUP",
|
||||
"LA", null, "Ontario", null, null, 1, BigDecimal.ZERO, BigDecimal.ZERO,
|
||||
null, "USD", null, null, null, null);
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> controller.update(user(Set.of(OverseasPermissions.LOOSEBOARD_CREATE)), "100", "LB1", request));
|
||||
assertEquals(403, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
private LocalLooseboardRequests.Create create() {
|
||||
return new LocalLooseboardRequests.Create("SHARED_TRUCK", "NO", "PORT_PICKUP", "LA", null,
|
||||
"Ontario", null, null, 1, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD",
|
||||
null, null, LocalDate.now(), null, "key-1");
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB", permissions);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
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.mapper.LocalLooseboardHistoryMapper;
|
||||
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOrderMapper;
|
||||
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 org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LocalLooseboardServiceTest {
|
||||
@Mock LocalLooseboardOrderMapper orders;
|
||||
@Mock LocalLooseboardHistoryMapper histories;
|
||||
private LocalLooseboardService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new LocalLooseboardService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createIsTenantScopedIdempotentAndStartsPendingAcceptance() {
|
||||
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("key-1"));
|
||||
|
||||
assertEquals("100", detail.summary().customerId());
|
||||
assertEquals(LocalLooseboardService.PENDING_ACCEPTANCE, detail.summary().status());
|
||||
assertEquals("SHARED_TRUCK", detail.summary().transportMode());
|
||||
verify(histories).insert(org.mockito.ArgumentMatchers.<LocalLooseboardHistory>argThat(value ->
|
||||
"CREATE".equals(value.getOperationType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryUpdateCannotChangeAcceptedOrder() {
|
||||
LocalLooseboardOrder order = order(LocalLooseboardService.DISPATCHED, 0L);
|
||||
when(orders.lockNo("100", "LB1")).thenReturn(order);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.update("100", "LB1", "7", update(0L)));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusTransitionWritesHistoryAndVersion() {
|
||||
LocalLooseboardOrder order = order(LocalLooseboardService.PENDING_ACCEPTANCE, 0L);
|
||||
when(orders.lockNo("100", "LB1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
|
||||
var detail = service.changeStatus("100", "LB1", "7",
|
||||
new LocalLooseboardRequests.ChangeStatus(0L, "DISPATCHED", "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())));
|
||||
}
|
||||
|
||||
private LocalLooseboardRequests.Create create(String key) {
|
||||
return new LocalLooseboardRequests.Create("SHARED_TRUCK", "YES", "WAREHOUSE_TO_WAREHOUSE",
|
||||
"Los Angeles", "Address A", "Ontario", "Address B", "40HQ", 12,
|
||||
new BigDecimal("100.000"), new BigDecimal("2.500000"), new BigDecimal("500.00"),
|
||||
"USD", "Alice", "+1-555-0100", LocalDate.now().plusDays(2), null, key);
|
||||
}
|
||||
|
||||
private LocalLooseboardRequests.Update update(long version) {
|
||||
return new LocalLooseboardRequests.Update(version, "SHARED_TRUCK", "YES", "WAREHOUSE_TO_WAREHOUSE",
|
||||
"Los Angeles", "Address A", "Ontario", "Address B", "40HQ", 12,
|
||||
new BigDecimal("100.000"), new BigDecimal("2.500000"), new BigDecimal("500.00"),
|
||||
"USD", "Alice", "+1-555-0100", LocalDate.now().plusDays(2), null);
|
||||
}
|
||||
|
||||
private LocalLooseboardOrder order(String status, long version) {
|
||||
LocalLooseboardOrder order = new LocalLooseboardOrder();
|
||||
order.setId(1L); order.setCustomerId("100"); order.setOrderNo("LB1");
|
||||
order.setTransportMode("SHARED_TRUCK"); order.setPalletRequired("YES");
|
||||
order.setTransportType("WAREHOUSE_TO_WAREHOUSE"); order.setOrigin("Los Angeles");
|
||||
order.setDestination("Ontario"); order.setPackageCount(12); order.setCurrency("USD");
|
||||
order.setStatus(status); order.setVersion(version); order.setCreatedAt(LocalDateTime.now());
|
||||
order.setUpdatedAt(order.getCreatedAt());
|
||||
return order;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.masterdata;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OverseasMasterDataOpenApiContractTest {
|
||||
@Test
|
||||
void documentsExchangeRateAndVehicleApisWithoutDeliveryFulfillment() throws Exception {
|
||||
String yaml = Files.readString(Path.of("src/main/resources/openapi/overseas-master-data-v1.yaml"));
|
||||
assertThat(yaml).contains("/api/platform/v1/last-leg/master-data/overseas-quotes:")
|
||||
.contains("缓存合作仓目录30分钟")
|
||||
.contains("/api/platform/v1/last-leg/master-data/exchange-rates:")
|
||||
.contains("/api/platform/v1/last-leg/master-data/vehicles:")
|
||||
.contains("platform:overseas-master-data:exchange-rate:export")
|
||||
.contains("platform:overseas-master-data:vehicle:status")
|
||||
.contains("不包含派送履约或车辆更换历史")
|
||||
.doesNotContain("deliveryOrders:")
|
||||
.doesNotContain("replacementHistory:");
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MonthlyExchangeRateSqlContractTest {
|
||||
@Test
|
||||
void baselineContainsRateAndHistoryTablesWithOptimisticLock() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS ll_monthly_exchange_rate (")
|
||||
.contains("UNIQUE KEY uk_ll_exchange_rate_month (rate_month)")
|
||||
.contains("record_version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本'")
|
||||
.contains("CREATE TABLE IF NOT EXISTS ll_monthly_exchange_rate_history (");
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformMonthlyExchangeRateControllerContractTest {
|
||||
@Test
|
||||
void exposesStablePlatformBaseAndCanonicalAuthorities() {
|
||||
RequestMapping base = PlatformMonthlyExchangeRateController.class.getAnnotation(RequestMapping.class);
|
||||
assertThat(base.value()).containsExactly("/api/platform/v1/last-leg/master-data/exchange-rates");
|
||||
Map<String, String> authorities = Stream.of(PlatformMonthlyExchangeRateController.class.getDeclaredMethods())
|
||||
.filter(method -> method.isAnnotationPresent(PreAuthorize.class))
|
||||
.collect(Collectors.toMap(method -> method.getName(), method -> method.getAnnotation(PreAuthorize.class).value()));
|
||||
assertThat(authorities.get("page")).contains("platform:overseas-master-data:exchange-rate:list");
|
||||
assertThat(authorities.get("summary")).contains("platform:overseas-master-data:exchange-rate:list");
|
||||
assertThat(authorities.get("detail")).contains("platform:overseas-master-data:exchange-rate:list");
|
||||
assertThat(authorities.get("create")).contains("platform:overseas-master-data:exchange-rate:create");
|
||||
assertThat(authorities.get("update")).contains("platform:overseas-master-data:exchange-rate:edit");
|
||||
assertThat(authorities.get("status")).contains("platform:overseas-master-data:exchange-rate:status");
|
||||
assertThat(authorities.get("export")).contains("platform:overseas-master-data:exchange-rate:export");
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.service;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.exchange.dto.MonthlyExchangeRateDtos;
|
||||
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRate;
|
||||
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateHistoryMapper;
|
||||
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MonthlyExchangeRateServiceTest {
|
||||
private MonthlyExchangeRateMapper mapper;
|
||||
private MonthlyExchangeRateService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(MonthlyExchangeRateMapper.class);
|
||||
service = new MonthlyExchangeRateService();
|
||||
ReflectionTestUtils.setField(service, "rates", mapper);
|
||||
ReflectionTestUtils.setField(service, "histories", mock(MonthlyExchangeRateHistoryMapper.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersion() {
|
||||
MonthlyExchangeRate value = new MonthlyExchangeRate(); value.setRecordVersion(4L); value.setRateMonth("2026-08");
|
||||
when(mapper.lockMonth("2026-08")).thenReturn(value);
|
||||
var request = new MonthlyExchangeRateDtos.Update(3L, new BigDecimal("7.18000000"), null);
|
||||
assertThatThrownBy(() -> service.update("2026-08", "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409 CONFLICT");
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.masterdata.quote.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformOverseasQuoteControllerContractTest {
|
||||
@Test
|
||||
void exposesStablePlatformBaseAndCanonicalAuthorities() {
|
||||
RequestMapping base = PlatformOverseasQuoteController.class.getAnnotation(RequestMapping.class);
|
||||
assertThat(base.value()).containsExactly("/api/platform/v1/last-leg/master-data/overseas-quotes");
|
||||
Map<String, String> authorities = Stream.of(PlatformOverseasQuoteController.class.getDeclaredMethods())
|
||||
.filter(method -> method.isAnnotationPresent(PreAuthorize.class))
|
||||
.collect(Collectors.toMap(method -> method.getName(),
|
||||
method -> method.getAnnotation(PreAuthorize.class).value()));
|
||||
assertThat(authorities.get("page")).contains("platform:overseas-master-data:quote:list");
|
||||
assertThat(authorities.get("detail")).contains("platform:overseas-master-data:quote:list");
|
||||
assertThat(authorities.get("warehouseOptions")).contains("platform:overseas-master-data:quote:list");
|
||||
assertThat(authorities.get("create")).contains("platform:overseas-master-data:quote:create");
|
||||
assertThat(authorities.get("update")).contains("platform:overseas-master-data:quote:edit");
|
||||
assertThat(authorities.get("status")).contains("platform:overseas-master-data:quote:status");
|
||||
assertThat(authorities.get("export")).contains("platform:overseas-master-data:quote:export");
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.lastleg.masterdata.quote.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.masterdata.quote.client.DomesticPartnerWarehouseClient;
|
||||
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.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PartnerWarehouseDirectoryTest {
|
||||
@Mock private DomesticPartnerWarehouseClient client;
|
||||
private PartnerWarehouseDirectory directory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
directory = new PartnerWarehouseDirectory();
|
||||
ReflectionTestUtils.setField(directory, "client", client);
|
||||
ReflectionTestUtils.setField(directory, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(directory, "ttl", Duration.ofMinutes(30));
|
||||
ReflectionTestUtils.setField(directory, "clock", Clock.fixed(
|
||||
Instant.parse("2026-08-20T00:00:00Z"), ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cachesDomesticOptionsForThirtyMinutesAndReturnsIdsAsStrings() {
|
||||
when(client.activePartners("internal-token")).thenReturn(ApiResponse.success(List.of(
|
||||
new DomesticPartnerWarehouseClient.Option(Long.valueOf("9007199254740993"),
|
||||
"US-LAX", "洛杉矶合作仓", "US"))));
|
||||
|
||||
assertThat(directory.options()).singleElement().satisfies(option -> {
|
||||
assertThat(option.warehouseId()).isEqualTo("9007199254740993");
|
||||
assertThat(option.warehouseCode()).isEqualTo("US-LAX");
|
||||
assertThat(option.warehouseName()).isEqualTo("洛杉矶合作仓");
|
||||
assertThat(option.countryCode()).isEqualTo("US");
|
||||
});
|
||||
directory.options();
|
||||
verify(client, times(1)).activePartners("internal-token");
|
||||
|
||||
ReflectionTestUtils.setField(directory, "clock", Clock.fixed(
|
||||
Instant.parse("2026-08-20T00:30:01Z"), ZoneOffset.UTC));
|
||||
directory.options();
|
||||
verify(client, times(2)).activePartners("internal-token");
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class VehicleSqlContractTest {
|
||||
@Test
|
||||
void baselineContainsVehicleMasterDataWithoutDeliveryHistory() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
String vehicleSql = sql.substring(sql.indexOf("CREATE TABLE IF NOT EXISTS ll_vehicle ("));
|
||||
assertThat(vehicleSql).contains("UNIQUE KEY uk_ll_vehicle_plate (plate_number)")
|
||||
.contains("record_version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本'")
|
||||
.contains("ownership VARCHAR(16) NOT NULL")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS ll_vehicle_delivery")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS ll_vehicle_replacement");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformVehicleControllerContractTest {
|
||||
@Test
|
||||
void exposesStablePlatformBaseAndCanonicalAuthorities() {
|
||||
RequestMapping base = PlatformVehicleController.class.getAnnotation(RequestMapping.class);
|
||||
assertThat(base.value()).containsExactly("/api/platform/v1/last-leg/master-data/vehicles");
|
||||
Map<String, String> authorities = Stream.of(PlatformVehicleController.class.getDeclaredMethods())
|
||||
.filter(method -> method.isAnnotationPresent(PreAuthorize.class))
|
||||
.collect(Collectors.toMap(method -> method.getName(), method -> method.getAnnotation(PreAuthorize.class).value()));
|
||||
assertThat(authorities.get("page")).contains("platform:overseas-master-data:vehicle:list");
|
||||
assertThat(authorities.get("detail")).contains("platform:overseas-master-data:vehicle:list");
|
||||
assertThat(authorities.get("create")).contains("platform:overseas-master-data:vehicle:create");
|
||||
assertThat(authorities.get("update")).contains("platform:overseas-master-data:vehicle:edit");
|
||||
assertThat(authorities.get("status")).contains("platform:overseas-master-data:vehicle:status");
|
||||
assertThat(authorities.get("export")).contains("platform:overseas-master-data:vehicle:export");
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.service;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.vehicle.dto.VehicleDtos;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.entity.Vehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.mapper.VehicleMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class VehicleServiceTest {
|
||||
private VehicleMapper mapper;
|
||||
private VehicleService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(VehicleMapper.class);
|
||||
service = new VehicleService();
|
||||
ReflectionTestUtils.setField(service, "vehicles", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeUpdating() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(2L, 1000L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409 CONFLICT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDecreasingOdometer() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(3L, 999L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownStatus() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
assertThatThrownBy(() -> service.changeStatus(1L, "9", new VehicleDtos.ChangeStatus("BUSY", 3L)))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
private static VehicleDtos.Update update(long version, long mileage) {
|
||||
return new VehicleDtos.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", null);
|
||||
}
|
||||
|
||||
private static Vehicle vehicle(long version, long mileage) {
|
||||
Vehicle value = new Vehicle(); value.setId(1L); value.setRecordVersion(version); value.setMileageKm(mileage);
|
||||
value.setStatus("IN_SERVICE"); return value;
|
||||
}
|
||||
}
|
||||
+3
@@ -21,5 +21,8 @@ class OverseasProcurementInventoryBoundaryTest {
|
||||
assertFalse(source.contains("stockIn("));
|
||||
assertFalse(source.contains("setAvailablePackages"));
|
||||
assertFalse(source.contains("availablePackages"));
|
||||
assertFalse(source.contains("addOutbound"));
|
||||
assertFalse(source.contains("OverseasProcurementOutbound"));
|
||||
assertFalse(source.contains("outboundQuantity"));
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.lastleg.procurement;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class OverseasProcurementOpenApiContractTest {
|
||||
@Test
|
||||
void procurementContractDocumentsCandidatesClearSemanticsAndExcludesOutbound() throws Exception {
|
||||
String yaml;
|
||||
try (var input = getClass().getResourceAsStream("/openapi/overseas-business-v1.yaml")) {
|
||||
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
assertTrue(yaml.contains("/customer-options:"));
|
||||
assertTrue(yaml.contains("/warehouse-options:"));
|
||||
assertTrue(yaml.contains("显式 null 或空串表示清空"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:purchase:list"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:purchase:edit"));
|
||||
assertFalse(yaml.contains("/outbounds"));
|
||||
assertFalse(yaml.contains("采购出库单"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contractContainsPlatformTruckWritesAndIndependentLooseboardDomain() throws Exception {
|
||||
String yaml;
|
||||
try (var input = getClass().getResourceAsStream("/openapi/overseas-business-v1.yaml")) {
|
||||
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
assertTrue(yaml.contains("platform:overseas-business:truck:create"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:truck:edit"));
|
||||
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"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:looseboard:edit"));
|
||||
assertTrue(yaml.contains("platform:overseas-business:looseboard:status"));
|
||||
assertFalse(yaml.contains("/api/platform/v1/last-leg/outbound"));
|
||||
assertFalse(yaml.contains("/api/platform/v1/last-leg/delivery"));
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -8,14 +8,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class OverseasProcurementSchemaContractTest {
|
||||
@Test
|
||||
void baselineContainsOrderOutboundAndReviewHistoryTables() throws Exception {
|
||||
void baselineContainsOrderAndOperationHistoryButNoProcurementOutbound() throws Exception {
|
||||
String sql;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
sql = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_outbound"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_status_history"));
|
||||
assertTrue(sql.contains("UNIQUE KEY uk_ll_overseas_procurement_outbound_no(customer_id,order_id,outbound_no)"));
|
||||
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -62,6 +62,15 @@ class OverseasProcurementControllerTest {
|
||||
verify(service).review("100", "HC1", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformUpdatePassesAuditedOperator() {
|
||||
var request = new OverseasProcurementRequests.Update(0L, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
platform.update(user(0L, "PLATFORM", Set.of(OverseasProcurementService.MANAGE_PERMISSION)),
|
||||
"100", "HC1", request);
|
||||
verify(service).update("100", "HC1", "7", request);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(long tenantId, String appCode, Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, tenantId, tenantId, 8L, "operator", appCode, "WEB", permissions);
|
||||
}
|
||||
|
||||
+69
-14
@@ -3,8 +3,9 @@ package com.baiyue.lastleg.procurement.service;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOrderMapper;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOutboundMapper;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.masterdata.quote.service.PartnerWarehouseDirectory;
|
||||
import com.baiyue.lastleg.masterdata.quote.dto.OverseasQuoteResponses;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -23,21 +24,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OverseasProcurementServiceTest {
|
||||
@Mock OverseasProcurementOrderMapper orders;
|
||||
@Mock OverseasProcurementOutboundMapper outbounds;
|
||||
@Mock OverseasProcurementStatusHistoryMapper histories;
|
||||
@Mock PartnerWarehouseDirectory warehouseDirectory;
|
||||
private OverseasProcurementService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new OverseasProcurementService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "outbounds", outbounds);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
}
|
||||
|
||||
@@ -49,7 +52,6 @@ class OverseasProcurementServiceTest {
|
||||
return 1;
|
||||
});
|
||||
when(orders.findNo(anyString(), anyString())).thenAnswer(ignored -> saved.get());
|
||||
when(outbounds.listOrder(anyString(), any())).thenReturn(List.of());
|
||||
when(histories.listOrder(anyString(), any())).thenReturn(List.of());
|
||||
|
||||
var detail = service.create("100", "7", new OverseasProcurementRequests.Create(
|
||||
@@ -69,21 +71,75 @@ class OverseasProcurementServiceTest {
|
||||
100L, null, null, null, null, null, null, null, null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.update("100", "HC1", request));
|
||||
() -> service.update("100", "HC1", "7", request));
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundCannotExceedDerivedInventory() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 100, 80);
|
||||
void explicitNullClearsOptionalFieldsAndWritesOperationHistory() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 100, 0);
|
||||
order.setProductUrl("https://example.test/old");
|
||||
when(orders.lockNo("100", "HC1")).thenReturn(order);
|
||||
when(orders.addOutbound(order.getId(), "100", 30)).thenReturn(0);
|
||||
var request = new OverseasProcurementRequests.AddOutbound(30L, "OUT-1", null, null,
|
||||
LocalDateTime.now(), null);
|
||||
when(orders.findNo("100", "HC1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
var request = new OverseasProcurementRequests.Update(0L, null, null,
|
||||
new ObjectMapper().nullNode(), new ObjectMapper().nullNode(), null,
|
||||
null, null, null, null, null, null, null, null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.addOutbound("100", "HC1", "7", request));
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
service.update("100", "HC1", "7", request);
|
||||
|
||||
assertEquals(null, order.getProductUrl());
|
||||
verify(histories).insert(org.mockito.ArgumentMatchers.<com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory>argThat(value ->
|
||||
"UPDATE".equals(value.getOperationType()) && value.getChangeDetail().contains("productUrl")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void omittedOptionalFieldKeepsExistingValue() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 100, 0);
|
||||
order.setProductUrl("https://example.test/keep");
|
||||
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.Update(0L, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
service.update("100", "HC1", "7", request);
|
||||
|
||||
assertEquals("https://example.test/keep", order.getProductUrl());
|
||||
verify(orders, never()).updateById(org.mockito.ArgumentMatchers.<OverseasProcurementOrder>any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseSelectionFreezesCanonicalCandidateName() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 100, 0);
|
||||
when(orders.lockNo("100", "HC1")).thenReturn(order);
|
||||
when(orders.findNo("100", "HC1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
when(warehouseDirectory.required(10L)).thenReturn(
|
||||
new OverseasQuoteResponses.PartnerWarehouseOption("10", "LAX-01", "洛杉矶采购仓", "US"));
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
var request = new OverseasProcurementRequests.Update(0L, null, null, null, null,
|
||||
null, mapper.getNodeFactory().textNode("10"), mapper.getNodeFactory().textNode("伪造名称"),
|
||||
null, null, null, null, null, null);
|
||||
|
||||
service.update("100", "HC1", "7", request);
|
||||
|
||||
assertEquals("10", order.getPurchaseWarehouseId());
|
||||
assertEquals("洛杉矶采购仓", order.getPurchaseWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerOptionsAreDeduplicatedSnapshotsFromProcurementOrders() {
|
||||
OverseasProcurementOrder option = new OverseasProcurementOrder();
|
||||
option.setCustomerId("100");
|
||||
option.setCustomerName("Acme");
|
||||
when(orders.customerOptions("ac", 20)).thenReturn(List.of(option));
|
||||
|
||||
var values = service.customerOptions(" ac ", 20);
|
||||
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("100", values.get(0).customerId());
|
||||
assertEquals("Acme", values.get(0).customerName());
|
||||
}
|
||||
|
||||
private OverseasProcurementOrder order(String status, long inbound, long outbound) {
|
||||
@@ -93,7 +149,6 @@ class OverseasProcurementServiceTest {
|
||||
order.setOrderNo("HC1");
|
||||
order.setStatus(status);
|
||||
order.setInboundQuantity(inbound);
|
||||
order.setOutboundQuantity(outbound);
|
||||
order.setPurchaseQuantity(500L);
|
||||
order.setVersion(0L);
|
||||
order.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
+35
-5
@@ -26,10 +26,13 @@ class SchemaChineseCommentContractTest {
|
||||
Path.of("src", "main", "resources", "db", "lastleg.sql"), StandardCharsets.UTF_8);
|
||||
String incremental = Files.readString(
|
||||
Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8);
|
||||
String overseasUnified = Files.readString(
|
||||
Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
List<TableDefinition> baselineTables = assertAllChineseComments(baseline, "基线 SQL", 55);
|
||||
assertAllChineseComments(incremental, "增量 SQL", 49);
|
||||
assertCommentMigrationCovers(incremental, baselineTables);
|
||||
assertAllChineseComments(overseasUnified, "海外统一增量 SQL", 10);
|
||||
assertCommentMigrationCovers(incremental, overseasUnified, baselineTables);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,21 +81,48 @@ class SchemaChineseCommentContractTest {
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static void assertCommentMigrationCovers(String incremental, List<TableDefinition> tables) {
|
||||
private static void assertCommentMigrationCovers(
|
||||
String incremental,
|
||||
String overseasUnified,
|
||||
List<TableDefinition> tables
|
||||
) {
|
||||
int marker = incremental.indexOf("2026-08-18 / 025 Chinese table and column comments");
|
||||
assertTrue(marker >= 0, "增量 SQL 缺少 025 中文备注迁移");
|
||||
String migration = incremental.substring(marker);
|
||||
for (TableDefinition table : tables) {
|
||||
assertTrue(migration.contains("table_name='" + table.name() + "'"),
|
||||
boolean createdInLegacy = hasCommentedCreate(migration, table.name());
|
||||
boolean createdInUnified = hasCommentedCreate(overseasUnified, table.name());
|
||||
assertTrue(createdInLegacy || createdInUnified
|
||||
|| migration.contains("table_name='" + table.name() + "'"),
|
||||
"025 备注迁移缺少表: " + table.name());
|
||||
if (createdInLegacy || createdInUnified) {
|
||||
continue;
|
||||
}
|
||||
for (String column : table.columns()) {
|
||||
assertTrue(migration.contains("table_name='" + table.name()
|
||||
+ "' AND column_name='" + column + "'"),
|
||||
boolean repaired = migration.contains("table_name='" + table.name()
|
||||
+ "' AND column_name='" + column + "'");
|
||||
boolean addedWithComment = hasCommentedAdd(migration, column)
|
||||
|| hasCommentedAdd(overseasUnified, column);
|
||||
assertTrue(repaired || addedWithComment,
|
||||
"025 备注迁移缺少字段: " + table.name() + "." + column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasCommentedCreate(String sql, String table) {
|
||||
return Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+" + Pattern.quote(table)
|
||||
+ "\\s*\\(.*?\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;")
|
||||
.matcher(sql).find();
|
||||
}
|
||||
|
||||
private static boolean hasCommentedAdd(String sql, String column) {
|
||||
return Pattern.compile(
|
||||
"(?is)ADD\\s+COLUMN\\s+" + Pattern.quote(column)
|
||||
+ "\\b[^;]*?COMMENT\\s+''?[^;]*[\\u4e00-\\u9fff][^;]*''?")
|
||||
.matcher(sql).find();
|
||||
}
|
||||
|
||||
private static boolean isConstraint(String definition) {
|
||||
String normalized = definition.stripLeading().toUpperCase();
|
||||
return normalized.startsWith("PRIMARY ") || normalized.startsWith("UNIQUE ")
|
||||
|
||||
+6
-35
@@ -7,12 +7,6 @@ import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -37,9 +31,6 @@ class LastlegOrderServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper details;
|
||||
@Mock LastlegOrderStatusHistoryMapper histories;
|
||||
@Mock DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Mock DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Mock DropshipCartonMapper dropshipCartons;
|
||||
LastlegOrderService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -48,35 +39,23 @@ class LastlegOrderServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orders);
|
||||
ReflectionTestUtils.setField(service, "detailMapper", details);
|
||||
ReflectionTestUtils.setField(service, "historyMapper", histories);
|
||||
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
|
||||
ReflectionTestUtils.setField(service, "dropshipSkuInventory", dropshipSkuInventory);
|
||||
ReflectionTestUtils.setField(service, "dropshipCartons", dropshipCartons);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailGroupsDropshipSkuAndCartonsByPallet() {
|
||||
void detailDoesNotExposeDropshipCartonsOrInternalEventIds() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
||||
order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.selectOne(any())).thenReturn(order);
|
||||
when(details.selectList(any())).thenReturn(List.of());
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
DropshipOrderProfile first = profile(1L, "SOURCE-P-1", 101L, "WH-P-1");
|
||||
DropshipOrderProfile second = profile(2L, "SOURCE-P-2", 102L, "WH-P-2");
|
||||
when(dropshipProfiles.listOrder("100", 11L)).thenReturn(List.of(first, second));
|
||||
DropshipSkuInventory sku = new DropshipSkuInventory();
|
||||
sku.setProfileId(1L); sku.setSkuCode("SKU-A"); sku.setPackageCount(1L); sku.setProductQuantity(10L);
|
||||
when(dropshipSkuInventory.listOrder("100", 11L)).thenReturn(List.of(sku));
|
||||
DropshipCarton carton = new DropshipCarton();
|
||||
carton.setProfileId(1L); carton.setSkuCode("SKU-A"); carton.setCartonMark("MARK-1");
|
||||
when(dropshipCartons.listOrder("100", 11L)).thenReturn(List.of(carton));
|
||||
|
||||
var response = service.detail("100", 11L);
|
||||
|
||||
assertEquals(2, response.dropshipPallets().size());
|
||||
assertEquals("SOURCE-P-1", response.dropshipPallets().get(0).sourcePalletNo());
|
||||
assertEquals(1, response.dropshipPallets().get(0).skuInventory().size());
|
||||
assertEquals("MARK-1", response.dropshipPallets().get(0).cartons().get(0).getCartonMark());
|
||||
assertTrue(response.dropshipPallets().get(1).skuInventory().isEmpty());
|
||||
assertEquals(11L, response.order().id());
|
||||
assertTrue(java.util.Arrays.stream(response.getClass().getRecordComponents())
|
||||
.noneMatch(component -> "dropshipPallets".equals(component.getName())));
|
||||
assertTrue(java.util.Arrays.stream(com.baiyue.lastleg.dto.LastlegOrderDetailResponse.History.class.getRecordComponents())
|
||||
.noneMatch(component -> "eventId".equals(component.getName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,12 +112,4 @@ class LastlegOrderServiceTest {
|
||||
return order;
|
||||
}
|
||||
|
||||
private static DropshipOrderProfile profile(long id, String sourcePalletNo,
|
||||
long warehousePalletId, String palletNo) {
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile();
|
||||
profile.setId(id); profile.setSourcePalletNo(sourcePalletNo);
|
||||
profile.setWarehousePalletId(warehousePalletId); profile.setPalletNo(palletNo);
|
||||
profile.setStatus("RECEIVED");
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -206,6 +206,7 @@ class WarehouseTransferServiceTest {
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(inventoryAllocations.lockTransfer("TENANT-A", 21L)).thenReturn(List.of(allocation));
|
||||
when(inventoryCommands.lockBalance("TENANT-A", 601L)).thenReturn(balance(601L, 101L, "IN-1"));
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
@@ -237,6 +238,7 @@ class WarehouseTransferServiceTest {
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("TENANT-A"), eq("USER-1"), stockIn.capture());
|
||||
assertEquals(5, stockIn.getValue().packages());
|
||||
assertEquals("SKU-TRANSFER", stockIn.getValue().skuCode());
|
||||
assertEquals("WTR-1", stockIn.getValue().businessOrderNo());
|
||||
assertEquals("WAREHOUSE_TRANSFER_RECEIPT:WTR-1:501", stockIn.getValue().idempotencyKey());
|
||||
assertEquals(WarehouseTransferService.RECEIVED, transfer.getStatus());
|
||||
@@ -350,7 +352,7 @@ class WarehouseTransferServiceTest {
|
||||
private static InventoryBalance balance(long id, long inboundOrderId, String inboundCode) {
|
||||
InventoryBalance balance = new InventoryBalance();
|
||||
balance.setId(id); balance.setCustomerId("TENANT-A"); balance.setInboundOrderId(inboundOrderId);
|
||||
balance.setInboundCode(inboundCode);
|
||||
balance.setInboundCode(inboundCode); balance.setSkuCode("SKU-TRANSFER");
|
||||
return balance;
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -50,6 +50,25 @@ class UsTruckOrderControllerTest {
|
||||
verify(service).changeStatus("100", "TK1", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformCreateUsesExplicitCustomerAndCreatePermission() {
|
||||
var request = new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null,
|
||||
"Ontario", null, "40HQ", 1, 0, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO,
|
||||
null, "USD", "Alice", "+1-555-0100", java.time.LocalDate.now(), null, "create-key");
|
||||
platform.create(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_CREATE)),
|
||||
"100", request);
|
||||
verify(service).create("100", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformUpdateUsesExplicitCustomerAndEditPermission() {
|
||||
var request = new UsTruckOrderRequests.Update(0L, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
platform.update(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_EDIT)),
|
||||
"100", "TK1", request);
|
||||
verify(service).update("100", "TK1", "7", request);
|
||||
}
|
||||
|
||||
private UsTruckOrderRequests.Query query() { return new UsTruckOrderRequests.Query(1, 20, null, null, null, null, null, null, null); }
|
||||
private AuthenticatedUser user(long tenant, String app, Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, tenant, tenant, 8L, "operator", app, "WEB", permissions);
|
||||
|
||||
Reference in New Issue
Block a user