Merge origin/dev and resolve backend conflicts
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg;
|
||||
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
class QueryBindingContractTest {
|
||||
@Test
|
||||
void mvcQueryRecordsExposeOneResolvableConstructor() {
|
||||
assertDoesNotThrow(() -> BeanUtils.getResolvableConstructor(DeliveryRequests.Query.class));
|
||||
assertDoesNotThrow(() -> BeanUtils.getResolvableConstructor(InboundPlanRequests.Query.class));
|
||||
}
|
||||
}
|
||||
+40
@@ -9,11 +9,14 @@ 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.LastlegOrderCommandService;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -32,6 +35,7 @@ import static org.mockito.Mockito.when;
|
||||
class LastlegOrderControllerTest {
|
||||
// Controller contract tests intentionally instantiate the controller without a Spring context here.
|
||||
private final LastlegOrderService service = mock(LastlegOrderService.class);
|
||||
private final LastlegOrderCommandService commandService = mock(LastlegOrderCommandService.class);
|
||||
private final LastlegOrderController controller = controller();
|
||||
private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query(
|
||||
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
@@ -59,6 +63,23 @@ class LastlegOrderControllerTest {
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdCommandPassesVersionedIdempotentRequestToSingleOrderService() {
|
||||
var request = new LastlegOrderCommandDtos.HoldRequest(true, true, "海关查验", 2);
|
||||
|
||||
controller.hold(user(0L, "PLATFORM"), "100", 11L, "hold-11", request);
|
||||
|
||||
verify(commandService).hold("100", 11L, "hold-11", request, "1", "tester");
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualNodeQueryUsesOrderScopeRatherThanContainerScope() {
|
||||
controller.manualNodes(user(0L, "PLATFORM"), "100", 11L);
|
||||
|
||||
verify(commandService).manualNodes("100", 11L);
|
||||
verify(service, never()).containerProgressDetail(org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperatorCanQueryCrossCustomerContainerSummaries() {
|
||||
var summaryQuery = new LastlegOrderRequests.ContainerProgressQuery(1, 20, "CONT-", null, null);
|
||||
@@ -68,6 +89,24 @@ class LastlegOrderControllerTest {
|
||||
verify(service).containerProgressPage(summaryQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerEndpointsUseCanonicalLastMileOrderAuthorities() throws Exception {
|
||||
var list = LastlegOrderController.class.getMethod("containerSummaries",
|
||||
AuthenticatedUser.class, LastlegOrderRequests.ContainerProgressQuery.class);
|
||||
var detail = LastlegOrderController.class.getMethod("containerSummaryDetail",
|
||||
AuthenticatedUser.class, String.class);
|
||||
var change = LastlegOrderController.class.getMethod("changeReceivingWarehouse",
|
||||
AuthenticatedUser.class, String.class, String.class,
|
||||
LastlegOrderRequests.ChangeReceivingWarehouse.class);
|
||||
|
||||
assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:list')",
|
||||
list.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:list')",
|
||||
detail.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:edit')",
|
||||
change.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerSummariesRejectMerchantIdentity() {
|
||||
var summaryQuery = new LastlegOrderRequests.ContainerProgressQuery(1, 20, null, null, null);
|
||||
@@ -130,6 +169,7 @@ class LastlegOrderControllerTest {
|
||||
ReflectionTestUtils.setField(controller, "consumer", mock(LoadingCompletedConsumer.class));
|
||||
ReflectionTestUtils.setField(controller, "shippingNodeConsumer", mock(ContainerShippingNodeConsumer.class));
|
||||
ReflectionTestUtils.setField(controller, "orderService", service);
|
||||
ReflectionTestUtils.setField(controller, "commandService", commandService);
|
||||
ReflectionTestUtils.setField(controller, "tokenGuard", mock(InternalServiceTokenGuard.class));
|
||||
return controller;
|
||||
}
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.baiyue.lastleg.customervisit.service;
|
||||
|
||||
import com.baiyue.lastleg.customervisit.dto.CustomerVisitRequests;
|
||||
import com.baiyue.lastleg.customervisit.entity.CustomerVisit;
|
||||
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;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomerVisitServiceTest {
|
||||
@Mock private CustomerVisitMapper visits;
|
||||
@Mock private DeliveryOrderMapper deliveryOrders;
|
||||
@Mock private DeliveryOperationMapper deliveryOperations;
|
||||
@Mock private LastlegOrderMapper lastMileOrders;
|
||||
private CustomerVisitService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CustomerVisitService();
|
||||
ReflectionTestUtils.setField(service, "visits", visits);
|
||||
ReflectionTestUtils.setField(service, "deliveryOrders", deliveryOrders);
|
||||
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
|
||||
ReflectionTestUtils.setField(service, "lastMileOrders", lastMileOrders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsRollbackFromDeliveredToWaitDeliveryWithoutWritingHistory() {
|
||||
DeliveryOrder delivery = delivery("DELIVERED");
|
||||
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
|
||||
|
||||
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus("WAIT_DELIVERY", 0L, "错误倒退")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(error -> assertThat(((ResponseStatusException) error).getStatusCode().value()).isEqualTo(409))
|
||||
.hasMessageContaining("DELIVERED").hasMessageContaining("WAIT_DELIVERY");
|
||||
|
||||
verify(deliveryOrders, never()).updateById(delivery);
|
||||
verifyNoInteractions(deliveryOperations);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsForwardTransitionWithStaleVersion() {
|
||||
DeliveryOrder delivery = delivery("WAIT_DELIVERY");
|
||||
delivery.setVersion(3L);
|
||||
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
|
||||
|
||||
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus("DELIVERING", 2L, null)))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("版本");
|
||||
|
||||
verify(deliveryOrders, never()).updateById(delivery);
|
||||
verifyNoInteractions(deliveryOperations);
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
private DeliveryOrder delivery(String status) {
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(11L);
|
||||
delivery.setCustomerId("1001");
|
||||
delivery.setDeliveryNo("DL-1");
|
||||
delivery.setStatus(status);
|
||||
return delivery;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -66,6 +66,6 @@ class DeliveryLongIdJsonContractTest {
|
||||
DELIVERY_ID,
|
||||
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, 0, null, null, null, null, null, null, null, null, 0, java.util.List.of());
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.lastleg.delivery;
|
||||
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DropshipOutboundCandidateMapperContractTest {
|
||||
@Test
|
||||
void candidateQueryIsSingleStatementAndFailsClosedOnEveryEligibilityFact() throws Exception {
|
||||
String sql = select("listOutboundCandidates", String.class, String.class);
|
||||
for (String token : new String[] {"p.customer_id=#{customerId}", "p.status='ACTIVE'",
|
||||
"o.order_type='ONE_CLICK_DROPSHIP'", "COALESCE(o.detained,0)=0", "pos.status='OCCUPIED'",
|
||||
"pos.available_packages>0", "s.package_count>0", "c.status='AVAILABLE'",
|
||||
"ll_dispatch_pallet_lock", "ll_warehouse_transfer_inventory_allocation",
|
||||
"ll_outbound_pallet_item", "pos.overseas_warehouse_id=#{warehouseId}", "LIMIT 500"}) {
|
||||
assertTrue(sql.contains(token), token);
|
||||
}
|
||||
assertTrue(sql.contains("LEAST(pos.available_packages,s.package_count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void creationLockUsesTheSameEligibilityContractAndLocksOneSkuDimension() throws Exception {
|
||||
String sql = select("lockOutboundCandidate", String.class, Long.class, String.class);
|
||||
assertTrue(sql.contains("AND pos.id=#{positionId}"));
|
||||
assertTrue(sql.contains("AND s.sku_code=#{skuCode}"));
|
||||
assertTrue(sql.contains("LIMIT 1 FOR UPDATE"));
|
||||
assertTrue(sql.contains("ll_dispatch_pallet_lock"));
|
||||
assertTrue(sql.contains("ll_outbound_pallet_item"));
|
||||
}
|
||||
|
||||
private String select(String method, Class<?>... parameterTypes) throws Exception {
|
||||
Select annotation = DropshipOrderProfileMapper.class.getMethod(method, parameterTypes)
|
||||
.getAnnotation(Select.class);
|
||||
assertNotNull(annotation);
|
||||
return String.join("", annotation.value());
|
||||
}
|
||||
}
|
||||
+41
-3
@@ -11,6 +11,12 @@ import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -25,8 +31,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class PlatformDispatchOutboundContractTest {
|
||||
@Test
|
||||
void outboundDetailAcceptsTheDocumentUrlWithoutPagingParameters() throws Exception {
|
||||
DeliveryTransferController controller = new DeliveryTransferController();
|
||||
ReflectionTestUtils.setField(controller, "service",
|
||||
mock(com.baiyue.lastleg.delivery.service.DeliveryTransferService.class));
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver())
|
||||
.build();
|
||||
AuthenticatedUser platform = new AuthenticatedUser(1000001L, 0L, 0L, 1L, "platform_admin",
|
||||
"PLATFORM", "WEB", Set.of(OverseasPermissions.OUTBOUND_LIST));
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(platform, null, Set.of()));
|
||||
try {
|
||||
mockMvc.perform(get("/api/platform/v1/last-leg/outbound-orders/OB-WH-20260907-030529"))
|
||||
.andExpect(status().isOk());
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformActionsHaveIndependentPermissionsAndNoCustomerParameter() throws Exception {
|
||||
Method update = DispatchController.class.getMethod("update", AuthenticatedUser.class,
|
||||
@@ -35,7 +64,7 @@ class PlatformDispatchOutboundContractTest {
|
||||
Method transfer = DeliveryTransferController.class.getMethod("transfer", AuthenticatedUser.class,
|
||||
String.class, DeliveryRequests.Transfer.class);
|
||||
Method outboundDetail = DeliveryTransferController.class.getMethod("outboundDetail",
|
||||
AuthenticatedUser.class, String.class, DeliveryRequests.PalletQuery.class);
|
||||
AuthenticatedUser.class, String.class);
|
||||
Method outboundCancel = DeliveryTransferController.class.getMethod("cancelOutbound",
|
||||
AuthenticatedUser.class, String.class, DeliveryRequests.Cancel.class);
|
||||
|
||||
@@ -43,6 +72,7 @@ class PlatformDispatchOutboundContractTest {
|
||||
assertPermission(cancel, OverseasPermissions.DISPATCH_CANCEL);
|
||||
assertPermission(transfer, OverseasPermissions.DISPATCH_TRANSFER_DELIVERY);
|
||||
assertPermission(outboundDetail, OverseasPermissions.OUTBOUND_LIST);
|
||||
assertEquals(2, outboundDetail.getParameterCount());
|
||||
assertPermission(outboundCancel, OverseasPermissions.OUTBOUND_CANCEL);
|
||||
for (Method method : Set.of(update, cancel, transfer, outboundDetail, outboundCancel)) {
|
||||
assertTrue(Arrays.stream(method.getParameters())
|
||||
@@ -60,15 +90,20 @@ class PlatformDispatchOutboundContractTest {
|
||||
void oneClickRequiresCustomerWhileOutboundListKeepsOptionalFilter() throws Exception {
|
||||
Method create = DeliveryTransferController.class.getMethod("createOneClick", AuthenticatedUser.class,
|
||||
String.class, DeliveryRequests.OneClickOutbound.class);
|
||||
Method candidates = DeliveryTransferController.class.getMethod("oneClickCandidates", AuthenticatedUser.class,
|
||||
String.class, String.class);
|
||||
Method page = DeliveryTransferController.class.getMethod("outboundPage", AuthenticatedUser.class,
|
||||
String.class, DeliveryRequests.Query.class);
|
||||
|
||||
assertPermission(create, OverseasPermissions.OUTBOUND_CREATE);
|
||||
assertPermission(candidates, OverseasPermissions.OUTBOUND_CREATE);
|
||||
assertPermission(page, OverseasPermissions.OUTBOUND_LIST);
|
||||
RequestParam createCustomer = create.getParameters()[1].getAnnotation(RequestParam.class);
|
||||
RequestParam pageCustomer = page.getParameters()[1].getAnnotation(RequestParam.class);
|
||||
assertNotNull(createCustomer);
|
||||
assertTrue(createCustomer.required());
|
||||
assertTrue(candidates.getParameters()[1].getAnnotation(RequestParam.class).required());
|
||||
assertFalse(candidates.getParameters()[2].getAnnotation(RequestParam.class).required());
|
||||
assertNotNull(pageCustomer);
|
||||
assertFalse(pageCustomer.required());
|
||||
|
||||
@@ -91,8 +126,7 @@ class PlatformDispatchOutboundContractTest {
|
||||
() -> dispatchController.cancel(merchant, "DSP-101"));
|
||||
ResponseStatusException outboundRejected = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> deliveryController.outboundDetail(merchant, "OUT-101",
|
||||
new DeliveryRequests.PalletQuery(1, 20, null)));
|
||||
() -> deliveryController.outboundDetail(merchant, "OUT-101"));
|
||||
|
||||
assertEquals(403, dispatchRejected.getStatusCode().value());
|
||||
assertEquals(403, outboundRejected.getStatusCode().value());
|
||||
@@ -137,6 +171,10 @@ class PlatformDispatchOutboundContractTest {
|
||||
for (String action : Set.of(dispatchAction, transfer, outboundDetail, outboundCancel)) {
|
||||
assertFalse(action.contains("name: customerId"));
|
||||
}
|
||||
assertTrue(outboundDetail.contains("pallets[].palletNo"));
|
||||
assertTrue(yaml.contains("/api/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates:"));
|
||||
assertTrue(yaml.contains("OneClickOutboundCandidate"));
|
||||
assertTrue(yaml.contains("单次 SQL 返回,不逐行补查"));
|
||||
assertTrue(yaml.contains("platform:overseas-warehouse:dispatch:edit"));
|
||||
assertTrue(yaml.contains("platform:overseas-warehouse:dispatch:cancel"));
|
||||
assertTrue(yaml.contains("platform:overseas-warehouse:dispatch:transfer-delivery"));
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DeliveryStatusPolicyTest {
|
||||
@Test
|
||||
void exposesOnlyServerAuthorizedActionsForEachState() {
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("WAIT_DELIVERY"))
|
||||
.containsExactly("START_DELIVERY", "COMPLETE_DELIVERY", "REJECT_DELIVERY", "CANCEL_DELIVERY");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERING"))
|
||||
.containsExactly("COMPLETE_DELIVERY", "REJECT_DELIVERY", "MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERED")).containsExactly("MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("RETURN_PENDING")).isEmpty();
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("CANCELLED")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalStatesCannotReenterTheDeliveryFlow() {
|
||||
assertThat(DeliveryStatusPolicy.canTransition("DELIVERED", "WAIT_DELIVERY")).isFalse();
|
||||
assertThat(DeliveryStatusPolicy.canTransition("DELIVERED", "DELIVERING")).isFalse();
|
||||
assertThat(DeliveryStatusPolicy.canTransition("REJECTED", "DELIVERING")).isFalse();
|
||||
assertThat(DeliveryStatusPolicy.canTransition("CANCELLED", "DELIVERED")).isFalse();
|
||||
}
|
||||
}
|
||||
+146
-14
@@ -19,6 +19,7 @@ import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
@@ -34,6 +35,8 @@ import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipOutboundCandidate;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -53,6 +56,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -68,6 +72,7 @@ class DeliveryTransferServiceTest {
|
||||
@Mock DeliveryOperationMapper deliveryOperations;
|
||||
@Mock InboundPlanPalletAllocationMapper planAllocations;
|
||||
@Mock InboundPlanLineMapper planLines;
|
||||
@Mock InboundPlanCartonMapper inboundCartons;
|
||||
@Mock LastlegOrderMapper tailOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper histories;
|
||||
@Mock LastlegOrderDetailMapper tailOrderDetails;
|
||||
@@ -77,6 +82,7 @@ class DeliveryTransferServiceTest {
|
||||
@Mock InventoryBalanceMapper inventoryBalances;
|
||||
@Mock DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Mock DropshipCartonMapper dropshipCartons;
|
||||
@Mock DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
|
||||
private DeliveryTransferService service;
|
||||
|
||||
@@ -92,6 +98,7 @@ class DeliveryTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
|
||||
ReflectionTestUtils.setField(service, "planAllocations", planAllocations);
|
||||
ReflectionTestUtils.setField(service, "planLines", planLines);
|
||||
ReflectionTestUtils.setField(service, "inboundCartons", inboundCartons);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
@@ -102,6 +109,7 @@ class DeliveryTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
|
||||
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
|
||||
ReflectionTestUtils.setField(service, "dropshipCartons", dropshipCartons);
|
||||
ReflectionTestUtils.setField(service, "dropshipSkuInventory", dropshipSkuInventory);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -140,6 +148,40 @@ class DeliveryTransferServiceTest {
|
||||
assertTrue(!result.replayed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferBackfillsReceivedPlanPalletsBeforeCheckingDispatchItems() {
|
||||
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
|
||||
when(dispatchOrders.lockGlobalByDispatchNo("DSP-101")).thenReturn(dispatch);
|
||||
when(dispatchItems.bindReceivedPallets("TENANT-A", 101L)).thenReturn(1);
|
||||
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(
|
||||
item(201L, 501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12)));
|
||||
|
||||
service.platformTransfer("DSP-101", "U-1", transferRequest("transfer-after-receiving", null));
|
||||
|
||||
var ordered = inOrder(dispatchItems);
|
||||
ordered.verify(dispatchItems).bindReceivedPallets("TENANT-A", 101L);
|
||||
ordered.verify(dispatchItems).findByDispatchOrder("TENANT-A", 101L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferCollectsLxCodesWhenAnInboundCodeBelongsToMultipleTailOrders() {
|
||||
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
|
||||
when(dispatchOrders.lockGlobalByDispatchNo("DSP-101")).thenReturn(dispatch);
|
||||
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(
|
||||
item(201L, 501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12)));
|
||||
LastlegOrder primary = new LastlegOrder();
|
||||
primary.setCustomerId("TENANT-A"); primary.setInboundCode("IN-A"); primary.setLxCode("LX-1");
|
||||
LastlegOrder derived = new LastlegOrder();
|
||||
derived.setCustomerId("TENANT-A"); derived.setInboundCode("IN-A"); derived.setLxCode(null);
|
||||
when(tailOrders.selectList(any())).thenReturn(List.of(primary, derived), List.of());
|
||||
|
||||
service.platformTransfer("DSP-101", "U-1", transferRequest("transfer-duplicate-inbound", null));
|
||||
|
||||
ArgumentCaptor<OutboundOrder> outbound = ArgumentCaptor.forClass(OutboundOrder.class);
|
||||
verify(outboundOrders).insert(outbound.capture());
|
||||
assertEquals("LX-1", outbound.getValue().getLxCodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameIdempotencyKeyReturnsExistingDocumentsWithoutWrites() {
|
||||
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
|
||||
@@ -235,20 +277,26 @@ class DeliveryTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundDetailDoesNotExposeExcludedPalletOrOutboundFulfillmentRows() {
|
||||
void outboundDetailIncludesPalletCodesForDisplay() {
|
||||
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "transfer-101");
|
||||
DeliveryOrder delivery = delivery(401L, outbound, "DEL-401");
|
||||
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(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(pallet));
|
||||
|
||||
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301",
|
||||
new DeliveryRequests.PalletQuery(1, 20, null));
|
||||
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
|
||||
|
||||
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());
|
||||
assertEquals(1, detail.pallets().size());
|
||||
assertEquals("IP-ONT8-20260907-000001-P001", detail.pallets().get(0).palletNo());
|
||||
assertEquals("IN-A", detail.pallets().get(0).inboundCode());
|
||||
assertEquals("ONT8", detail.pallets().get(0).destinationWarehouseCode());
|
||||
assertEquals("SKU-1", detail.pallets().get(0).skuCode());
|
||||
assertEquals(12, detail.pallets().get(0).packageCount());
|
||||
verify(outboundPallets).findByOutbound("TENANT-A", 301L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -258,8 +306,7 @@ class DeliveryTransferServiceTest {
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
|
||||
|
||||
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301",
|
||||
new DeliveryRequests.PalletQuery(1, 20, null));
|
||||
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
|
||||
|
||||
assertEquals("OUT-301", detail.outbound().outboundNo());
|
||||
verify(deliveryOrders).lockByOutbound("TENANT-A", 301L);
|
||||
@@ -358,12 +405,8 @@ class DeliveryTransferServiceTest {
|
||||
|
||||
@Test
|
||||
void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() {
|
||||
InventoryPosition position = new InventoryPosition();
|
||||
position.setId(901L); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
|
||||
position.setPalletId(501L); position.setPalletNo("PLT-501"); position.setAvailablePackages(1L);
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
|
||||
when(inventoryPositions.selectById(901L)).thenReturn(position);
|
||||
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
|
||||
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());
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick(
|
||||
@@ -371,9 +414,43 @@ class DeliveryTransferServiceTest {
|
||||
new DeliveryRequests.OneClickItem(901L, 1L, "MIXED")), null, "one-click-mixed")));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
assertTrue(exception.getReason().contains("一件代发可用箱数不足"));
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickCandidatesUseOneAuthoritativeQueryAndExposeStableBusinessFields() {
|
||||
DropshipOutboundCandidate candidate = dropshipCandidate(901L, 1001L, "WH-1", "DS-A002", "SKU-1", 3L);
|
||||
candidate.setOverseasWarehouseName("Los Angeles"); candidate.setInboundCode("IN-1");
|
||||
candidate.setProductName("Product 1");
|
||||
when(dropshipProfiles.listOutboundCandidates("TENANT-A", "WH-1")).thenReturn(List.of(candidate));
|
||||
|
||||
List<DeliveryResponses.OneClickCandidate> result = service.oneClickCandidates("TENANT-A", " WH-1 ");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(901L, result.get(0).inventoryPositionId());
|
||||
assertEquals("DS-A002", result.get(0).palletNo());
|
||||
assertEquals("SKU-1", result.get(0).skuCode());
|
||||
assertEquals(3L, result.get(0).availablePackages());
|
||||
verify(dropshipProfiles).listOutboundCandidates("TENANT-A", "WH-1");
|
||||
verify(inventoryPositions, never()).selectById(any());
|
||||
}
|
||||
|
||||
@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(
|
||||
new DeliveryRequests.OneClickItem(901L, 1L, "MIXED")), null, "ordinary-stock")));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
assertEquals("所选库存不是可用的一件代发库存,或已被其他作业占用", exception.getReason());
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
verify(outboundPallets, never()).insert(any(OutboundPalletItem.class));
|
||||
verify(pdaTasks, never()).insert(any(OutboundPdaTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundPdaListAndDetailUseOnlyBoundWarehouse() throws Exception {
|
||||
assertNotNull(DeliveryTransferService.class.getMethod("pdaPick", String.class, String.class,
|
||||
@@ -393,6 +470,53 @@ class DeliveryTransferServiceTest {
|
||||
verify(pdaTasks).listByWarehouse("WH-1", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundNumberResolvesDispatchWorkflowInBoundWarehouse() {
|
||||
OutboundOrder outbound = outbound(301L, 401L, "DSP-401", "OUT-301", "transfer-key");
|
||||
outbound.setOverseasWarehouseId("WH-1");
|
||||
outbound.setOutboundType("DISPATCH_DELIVERY");
|
||||
DispatchOrder dispatch = new DispatchOrder();
|
||||
dispatch.setId(401L); dispatch.setDispatchNo("DSP-401"); dispatch.setOverseasWarehouseId("WH-1");
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
when(dispatchOrders.findGlobalByDispatchNo("DSP-401")).thenReturn(dispatch);
|
||||
|
||||
DeliveryResponses.PdaTaskResolution result = service.resolvePdaTask("WH-1", "OUT-301");
|
||||
|
||||
assertEquals("DISPATCH", result.workflow());
|
||||
assertEquals("DSP-401", result.dispatchNo());
|
||||
assertEquals("OUT-301", result.outboundNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundNumberResolvesOneClickTaskInBoundWarehouse() {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");
|
||||
outbound.setOverseasWarehouseId("WH-1");
|
||||
outbound.setOutboundType("ONE_CLICK_DROPSHIP");
|
||||
OutboundPdaTask task = new OutboundPdaTask();
|
||||
task.setCustomerId("TENANT-A"); task.setOutboundOrderId(301L); task.setTaskNo("PDA-701");
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
when(pdaTasks.findByOutboundAndWarehouse("TENANT-A", 301L, "WH-1")).thenReturn(task);
|
||||
|
||||
DeliveryResponses.PdaTaskResolution result = service.resolvePdaTask("WH-1", "OUT-301");
|
||||
|
||||
assertEquals("OUTBOUND", result.workflow());
|
||||
assertEquals("PDA-701", result.taskNo());
|
||||
assertEquals("OUT-301", result.outboundNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaTaskResolutionDoesNotLeakAnotherWarehouse() {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");
|
||||
outbound.setOverseasWarehouseId("WH-2");
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.resolvePdaTask("WH-1", "OUT-301"));
|
||||
|
||||
assertEquals(404, exception.getStatusCode().value());
|
||||
verify(pdaTasks, never()).findByOutboundAndWarehouse(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundPdaDetailReturnsEveryPickablePositionWithStableBusinessCodes() {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");
|
||||
@@ -565,4 +689,12 @@ class DeliveryTransferServiceTest {
|
||||
value.setInboundCode(inboundCode); value.setOrderRefs("LMO-1"); value.setPackageCount(packages);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static DropshipOutboundCandidate dropshipCandidate(Long positionId, Long profileId, String warehouseId,
|
||||
String palletNo, String sku, Long packages) {
|
||||
DropshipOutboundCandidate value = new DropshipOutboundCandidate();
|
||||
value.setInventoryPositionId(positionId); value.setProfileId(profileId); value.setWarehousePalletId(501L);
|
||||
value.setOverseasWarehouseId(warehouseId); value.setPalletNo(palletNo); value.setSkuCode(sku);
|
||||
value.setAvailablePackages(packages); return value;
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -35,6 +35,20 @@ class DispatchCandidateMapperContractTest {
|
||||
assertTrue(sql.contains("pos.available_packages > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateWarehouseComesFromPhysicalInventoryOrFrozenInboundPlan() throws Exception {
|
||||
for (Method method : List.of(
|
||||
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
|
||||
String.class, String.class, String.class),
|
||||
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL"));
|
||||
assertTrue(sql.contains("pos.overseas_warehouse_id"));
|
||||
assertTrue(sql.contains("CAST(ip.overseas_warehouse_id AS CHAR)"));
|
||||
assertTrue(sql.contains("ll_inventory_balance"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertConstructorColumnOrder(Method method) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
int previous = -1;
|
||||
|
||||
+67
-2
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchDraft;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchDraftContainer;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
||||
@@ -16,6 +18,8 @@ import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -32,6 +36,7 @@ import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -49,6 +54,7 @@ class DispatchServiceTest {
|
||||
@Mock DispatchDraftMapper drafts;
|
||||
@Mock DispatchDraftContainerMapper draftContainers;
|
||||
@Mock DispatchPdaTaskMapper pdaTasks;
|
||||
@Mock LastlegOrderMapper tailOrders;
|
||||
private DispatchService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -61,8 +67,26 @@ class DispatchServiceTest {
|
||||
ReflectionTestUtils.setField(service, "drafts", drafts);
|
||||
ReflectionTestUtils.setField(service, "draftContainers", draftContainers);
|
||||
ReflectionTestUtils.setField(service, "pdaTasks", pdaTasks);
|
||||
ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
|
||||
lenient().when(tailOrders.selectList(any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preArrangeRejectsAHeldTailOrderBeforeCreatingDispatchDocuments() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
LastlegOrder held = new LastlegOrder();
|
||||
held.setId(2L); held.setCustomerId("100"); held.setOrderNo("LMO-HOLD-1"); held.setDetained(true);
|
||||
when(tailOrders.selectList(any())).thenReturn(List.of(held));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
|
||||
null, null, null)));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
assertEquals("暂扣订单不可创建排车或出库计划:LMO-HOLD-1", exception.getReason());
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -127,6 +151,21 @@ class DispatchServiceTest {
|
||||
verify(items).insert(any(DispatchItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preArrangeRejectsPalletWithoutResolvedOverseasWarehouseWithPreciseReason() {
|
||||
DispatchPalletCandidate unresolved = new DispatchPalletCandidate(1L, 10L, 20L, 2L,
|
||||
null, null, "LMO1", "LMO1", "RK1", "LIP1-P001", "ONT8", "ONT8", "ONT8", "ONT8",
|
||||
12, new BigDecimal("1.2000"), "RECEIVED");
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(unresolved));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.preArrange("100", "7",
|
||||
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
|
||||
|
||||
assertEquals("所选托盘中存在尚未关联海外仓的托盘", exception.getReason());
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingPalletFromOwnedDraftImmediatelyReleasesLock() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
@@ -145,15 +184,41 @@ class DispatchServiceTest {
|
||||
void preArrangeConvertsOwnedDraftLockInsteadOfInsertingAnotherLock() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
DispatchPalletLock lock = new DispatchPalletLock();
|
||||
lock.setDraftToken("draft-1"); lock.setOperatorId("7"); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10");
|
||||
lock.setDraftToken("draft-1"); lock.setDraftContainerId(21L); lock.setOperatorId("7"); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10");
|
||||
DispatchDraft draft = new DispatchDraft();
|
||||
draft.setId(31L); draft.setDraftToken("draft-1"); draft.setOperatorId("7"); draft.setStatus("ACTIVE");
|
||||
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"));
|
||||
|
||||
assertNotNull(lock.getDispatchOrderId());
|
||||
assertEquals(null, lock.getDraftToken());
|
||||
assertNull(lock.getDraftToken());
|
||||
assertNull(lock.getDraftContainerId());
|
||||
assertEquals("RELEASED", draft.getStatus());
|
||||
assertNotNull(draft.getReleasedAt());
|
||||
verify(locks).updateById(lock);
|
||||
verify(locks, never()).insert(any(DispatchPalletLock.class));
|
||||
verify(draftContainers).delete(any(LambdaQueryWrapper.class));
|
||||
verify(drafts).updateById(draft);
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeDraftOnlyRestoresLocksStillOwnedByThatDraft() {
|
||||
DispatchDraft draft = new DispatchDraft();
|
||||
draft.setDraftToken("draft-1"); draft.setOperatorId("7"); draft.setStatus("ACTIVE");
|
||||
DispatchDraftContainer container = new DispatchDraftContainer();
|
||||
container.setId(21L); container.setDraftToken("draft-1"); container.setCapacity(12);
|
||||
DispatchPalletLock owned = new DispatchPalletLock();
|
||||
owned.setPlanPalletId(10L); owned.setDraftToken("draft-1"); owned.setDraftContainerId(21L);
|
||||
when(drafts.findActiveByOperator("7")).thenReturn(List.of(draft));
|
||||
when(draftContainers.findByToken("draft-1")).thenReturn(List.of(container));
|
||||
when(locks.findActiveByDraftContainer("draft-1", 21L)).thenReturn(List.of(owned));
|
||||
|
||||
var result = service.activeDrafts("7");
|
||||
|
||||
assertEquals(List.of("10"), result.get(0).containers().get(0).palletIds());
|
||||
verify(locks).findActiveByDraftContainer("draft-1", 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-1
@@ -141,7 +141,8 @@ class DropshipOrderServiceTest {
|
||||
|
||||
private static InboundStockReceipt receipt(long palletId, String palletNo, long positionId, long packages) {
|
||||
InboundStockReceipt receipt = new InboundStockReceipt();
|
||||
receipt.setOrderId(10L); receipt.setWarehousePalletId(palletId); receipt.setPalletNo(palletNo);
|
||||
receipt.setOrderId(10L); receipt.setWarehousePalletId(palletId);
|
||||
receipt.setPalletNo("WH-" + palletId); receipt.setSourcePalletNo(palletNo);
|
||||
receipt.setInventoryPositionId(positionId); receipt.setReceivedPackages(packages);
|
||||
return receipt;
|
||||
}
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.baiyue.lastleg.exceptionorder.service;
|
||||
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderResponses;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrderCommand;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionReturnItem;
|
||||
import com.baiyue.lastleg.exceptionorder.gateway.ExceptionEvidenceFileGateway;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderCommandMapper;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionReturnItemMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
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.mapper.LastlegOrderMapper;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExceptionOrderLifecycleTest {
|
||||
@Mock private ExceptionOrderMapper exceptionOrders;
|
||||
@Mock private ExceptionOrderCommandMapper commands;
|
||||
@Mock private ExceptionReturnItemMapper returnItems;
|
||||
@Mock private InventoryCommandService inventoryCommands;
|
||||
@Mock private ExceptionEvidenceFileGateway evidenceFiles;
|
||||
@Mock private DeliveryOrderMapper deliveryOrders;
|
||||
@Mock private DeliveryOperationMapper deliveryOperations;
|
||||
@Mock private LastlegOrderMapper lastMileOrders;
|
||||
private ExceptionOrderService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ExceptionOrderService();
|
||||
objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
ReflectionTestUtils.setField(service, "exceptionOrders", exceptionOrders);
|
||||
ReflectionTestUtils.setField(service, "commands", commands);
|
||||
ReflectionTestUtils.setField(service, "returnItems", returnItems);
|
||||
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
|
||||
ReflectionTestUtils.setField(service, "evidenceFiles", evidenceFiles);
|
||||
ReflectionTestUtils.setField(service, "deliveryOrders", deliveryOrders);
|
||||
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
|
||||
ReflectionTestUtils.setField(service, "lastMileOrders", lastMileOrders);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsPendingExceptionWithVersionGateAndPersistsIdempotentResult() {
|
||||
ExceptionOrder order = order("PENDING", 0);
|
||||
when(commands.findByKey("1001", "START_PROCESSING", "start-1")).thenReturn(null);
|
||||
when(exceptionOrders.lockByNo("1001", "EX-1")).thenReturn(order);
|
||||
|
||||
var result = service.startProcessing("1001", "9001", "EX-1",
|
||||
new ExceptionOrderRequests.StartProcessing(0, "start-1", "联系客户并核对退件"));
|
||||
|
||||
assertThat(result.status()).isEqualTo("PROCESSING");
|
||||
assertThat(result.version()).isEqualTo(1);
|
||||
assertThat(result.replayed()).isFalse();
|
||||
verify(exceptionOrders).updateById(order);
|
||||
ArgumentCaptor<ExceptionOrderCommand> command = ArgumentCaptor.forClass(ExceptionOrderCommand.class);
|
||||
verify(commands).insert(command.capture());
|
||||
assertThat(command.getValue().getResultJson()).contains("PROCESSING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionWithoutMutation() {
|
||||
when(commands.findByKey("1001", "START_PROCESSING", "start-2")).thenReturn(null);
|
||||
when(exceptionOrders.lockByNo("1001", "EX-1")).thenReturn(order("PENDING", 2));
|
||||
|
||||
assertThatThrownBy(() -> service.startProcessing("1001", "9001", "EX-1",
|
||||
new ExceptionOrderRequests.StartProcessing(1, "start-2", null)))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("版本");
|
||||
|
||||
verify(exceptionOrders, never()).updateById(any(ExceptionOrder.class));
|
||||
verify(commands, never()).insert(any(ExceptionOrderCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysSameCommandWithoutUpdatingAgain() throws Exception {
|
||||
ExceptionOrderResponses.CommandResult saved = new ExceptionOrderResponses.CommandResult(
|
||||
1L, "EX-1", "PROCESSING", 1, null, 0, null, false);
|
||||
ExceptionOrderRequests.StartProcessing request =
|
||||
new ExceptionOrderRequests.StartProcessing(0, "start-3", null);
|
||||
ExceptionOrderCommand command = new ExceptionOrderCommand();
|
||||
command.setRequestHash(hashFor(request));
|
||||
command.setResultJson(objectMapper.writeValueAsString(saved));
|
||||
when(commands.findByKey("1001", "START_PROCESSING", "start-3")).thenReturn(command);
|
||||
|
||||
var replayed = service.startProcessing("1001", "9001", "EX-1", request);
|
||||
|
||||
assertThat(replayed.replayed()).isTrue();
|
||||
assertThat(replayed.status()).isEqualTo("PROCESSING");
|
||||
verify(exceptionOrders, never()).lockByNo(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesWarehouseReturnOnlyAfterInventoryRestoreSucceeds() {
|
||||
ExceptionOrder order = order("PROCESSING", 1);
|
||||
order.setExceptionType("RETURN"); order.setReturnDirection("OVERSEAS_WAREHOUSE");
|
||||
order.setOutboundNo("OUT-1"); order.setOverseasWarehouseId("WH-1"); order.setInboundOrderId(44L);
|
||||
order.setDeliveryNo("DL-1");
|
||||
ExceptionReturnItem source = new ExceptionReturnItem();
|
||||
source.setOutboundPalletItemId(51L); source.setInventoryPositionId(61L);
|
||||
source.setSkuCode("SKU-1"); source.setApprovedReturnPackages(2L);
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(71L); delivery.setCustomerId("1001"); delivery.setDeliveryNo("DL-1");
|
||||
delivery.setStatus("RETURN_PENDING"); delivery.setVersion(2L);
|
||||
LocalDateTime completedAt = LocalDateTime.of(2026, 9, 8, 18, 30);
|
||||
when(commands.findByKey("1001", "COMPLETE", "complete-1")).thenReturn(null);
|
||||
when(exceptionOrders.lockByNo("1001", "EX-1")).thenReturn(order);
|
||||
when(returnItems.findByException("1001", 1L)).thenReturn(List.of(source));
|
||||
when(inventoryCommands.restoreReturnedInventory(any(), any(), any(ReturnedInventoryRestoreRequest.class)))
|
||||
.thenReturn(new ReturnedInventoryRestoreResponse(81L, List.of(91L), 0, 2, 2, false, completedAt));
|
||||
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
|
||||
when(lastMileOrders.selectList(any())).thenReturn(List.of());
|
||||
ExceptionOrderRequests.Complete request = new ExceptionOrderRequests.Complete(1, "complete-1", "已退回入仓",
|
||||
List.of(101L), List.of(new ExceptionOrderRequests.ReturnReceipt(51L, 61L, 2, 201L,
|
||||
"PLT-201", 301L, "LOC-301")));
|
||||
|
||||
var result = service.complete("1001", "9001", "EX-1", request);
|
||||
|
||||
assertThat(result.status()).isEqualTo("COMPLETED");
|
||||
assertThat(result.inventoryAdjustmentId()).isEqualTo(81L);
|
||||
assertThat(result.restoredPackages()).isEqualTo(2);
|
||||
assertThat(delivery.getStatus()).isEqualTo("RETURNED");
|
||||
verify(evidenceFiles).validate("1001", 1L, 101L);
|
||||
verify(inventoryCommands).restoreReturnedInventory(any(), any(), any(ReturnedInventoryRestoreRequest.class));
|
||||
}
|
||||
|
||||
private String hashFor(Object request) {
|
||||
return (String) ReflectionTestUtils.invokeMethod(service, "hash", request);
|
||||
}
|
||||
|
||||
private ExceptionOrder order(String status, long version) {
|
||||
ExceptionOrder order = new ExceptionOrder();
|
||||
order.setId(1L); order.setCustomerId("1001"); order.setExceptionNo("EX-1");
|
||||
order.setStatus(status); order.setVersion(version);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
+7
@@ -27,6 +27,13 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
|
||||
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
|
||||
assertTrue(schema.contains("expected_loading_at"));
|
||||
assertTrue(schema.contains("planned_arrival_date"));
|
||||
assertTrue(schema.contains("creation_mode"));
|
||||
assertTrue(schema.contains("pallet_type"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_command_record"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_hold_history"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_manual_node"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_manual_node_history"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
|
||||
assertTrue(schema.contains("fba_warehouse_code"));
|
||||
assertFalse(schema.contains("cabinet_no"));
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.lastleg.inboundplan;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
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 TailPlanCommandOpenApiContractTest {
|
||||
@Test
|
||||
void exposesOnlyLatestManualPlanHoldAndEightRowNodeContracts() throws Exception {
|
||||
try (var stream = getClass().getClassLoader()
|
||||
.getResourceAsStream("openapi/overseas-business-v1.yaml")) {
|
||||
assertTrue(stream != null);
|
||||
String api = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/inbound-plans/manual:"));
|
||||
assertFalse(api.contains("/api/platform/v1/last-leg/inbound-plans/convert:"));
|
||||
assertFalse(api.contains("plannedWarehouseId"));
|
||||
assertFalse(api.contains("plannedWarehouseCode"));
|
||||
assertFalse(api.contains("plannedWarehouseName"));
|
||||
int manualSchemaStart = api.indexOf(" ManualInboundPlanRequest:");
|
||||
int manualSchemaEnd = api.indexOf("\n PageMetadata:", manualSchemaStart);
|
||||
assertTrue(manualSchemaStart >= 0 && manualSchemaEnd > manualSchemaStart);
|
||||
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("inboundCode"));
|
||||
assertFalse(java.util.Arrays.stream(ManualInboundPlanRequest.class.getRecordComponents())
|
||||
.anyMatch(component -> "inboundCode".equals(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:"));
|
||||
assertTrue(api.contains("minItems: 8"));
|
||||
assertTrue(api.contains("maxItems: 8"));
|
||||
assertTrue(api.contains("ACTUAL_ARRIVAL_WAITING_UNLOAD"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-22
@@ -3,8 +3,8 @@ package com.baiyue.lastleg.inboundplan.controller;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -61,31 +62,19 @@ class InboundPlanControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformConversionRequiresAnExplicitCustomer() {
|
||||
assertThatThrownBy(() -> controller.convert(platform(), new ConvertInboundPlanRequest(2L, null)))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("平台生成入库计划必须指定客户");
|
||||
verify(service, never()).convert(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
void platformManualPlanPassesVersionedIdempotentCommand() {
|
||||
ManualInboundPlanRequest request = manualRequest();
|
||||
controller.manual(platform(), "manual-key", request);
|
||||
verify(service).convertManual(request, "manual-key", "7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformConversionPassesTheProvidedLongCustomerId() {
|
||||
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(9223372036854775807L,
|
||||
"193000000000200003");
|
||||
|
||||
controller.convert(platform(), request);
|
||||
|
||||
verify(service).convert("193000000000200003", 9223372036854775807L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerConversionIgnoresRequestedCustomerAndUsesAuthenticatedScope() {
|
||||
void merchantCannotCreateManualPlan() {
|
||||
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
|
||||
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(2L, "200");
|
||||
|
||||
controller.convert(customer, request);
|
||||
|
||||
verify(service).convert("100", 2L);
|
||||
assertThatThrownBy(() -> controller.manual(customer, "manual-key", manualRequest()))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
verify(service, never()).convertManual(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,4 +98,9 @@ class InboundPlanControllerTest {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||
Set.of("platform:overseas-warehouse:inbound-order:list"));
|
||||
}
|
||||
|
||||
private static ManualInboundPlanRequest manualRequest() {
|
||||
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
|
||||
LocalDate.of(2026, 9, 8), 2, "STANDARD", false, true, true, "加急入仓");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class InboundPlanPermissionContractTest {
|
||||
@Test
|
||||
void conversionUsesCanonicalInboundOrderCreateAuthority() {
|
||||
Method method = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals("convert"))
|
||||
.filter(candidate -> candidate.getName().equals("manual"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
|
||||
+29
@@ -17,5 +17,34 @@ class InboundPlanOrderMapperContractTest {
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
|
||||
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
|
||||
assertTrue(sql.contains("0 AS destinationMismatchCount"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_name AS overseasWarehouseName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingWarehouseSnapshotIsReadFromTenantScopedOrderFact() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("findReceivingWarehouse", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("overseas_warehouse_name AS overseasWarehouseName"));
|
||||
assertTrue(sql.contains("customer_id=#{customerId} AND id=#{orderId}"));
|
||||
assertTrue(!sql.contains("destination_warehouse"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanLocksEveryOrderInTheSelectedStableContainer() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("lockContainerWarehouseFacts", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("c.source_container_booking_id=target.source_container_booking_id"));
|
||||
assertTrue(sql.contains("target.customer_id=#{customerId} AND target.id=#{orderId}"));
|
||||
assertTrue(sql.contains("c.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("ORDER BY c.customer_id,c.id FOR UPDATE"));
|
||||
}
|
||||
}
|
||||
|
||||
+70
-1
@@ -11,6 +11,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
class HttpWarehouseResourceGatewayTest {
|
||||
@@ -32,7 +33,9 @@ class HttpWarehouseResourceGatewayTest {
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-1",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3}}
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3,
|
||||
"locationType":"STORAGE","locationStatus":"IDLE","reservable":true,
|
||||
"reservationBlockReason":null}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
WarehouseResourceGateway.WarehousePalletReference result = gateway.resolvePallet("PAL-1");
|
||||
@@ -43,6 +46,40 @@ class HttpWarehouseResourceGatewayTest {
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRejectsPalletWhoseLocationIsNotReservable() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-BLOCKED"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-BLOCKED",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3,
|
||||
"locationType":"STORAGE","locationStatus":"OCCUPIED","reservable":false,
|
||||
"reservationBlockReason":"LOCATION_NOT_IDLE"}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.resolvePallet("PAL-BLOCKED"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Pallet location is not idle");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveFailsClosedWhenWarehouseOmitsReservationFacts() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-UNKNOWN",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.resolvePallet("PAL-UNKNOWN"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("availability could not be verified");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureDropshipStillMapsWarehouseEntityId() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/ensure-dropship"))
|
||||
@@ -72,4 +109,36 @@ class HttpWarehouseResourceGatewayTest {
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reserveMapsWarehouseLocationConflictToStableConflict() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks"))
|
||||
.andRespond(withStatus(org.springframework.http.HttpStatus.CONFLICT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("""
|
||||
{"code":"A0409","message":"Location is not idle","data":null}
|
||||
"""));
|
||||
|
||||
assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L,
|
||||
"LASTLEG_INBOUND", "plan:5", "reservation-1"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Warehouse resource reservation was rejected");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reserveMapsBusinessConflictEnvelopeToStableConflict() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"A0409","message":"Location is not idle","data":null}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L,
|
||||
"LASTLEG_INBOUND", "plan:5", "reservation-1"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Location is not idle");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
@@ -167,6 +167,41 @@ class InboundPdaWorkflowServiceTest {
|
||||
targetPallet.getWarehouseLocationId(), "assign-wrong-warehouse")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedAssignmentReplacesPlanPalletNumberWithPhysicalWarehouseCode() {
|
||||
targetPallet.setPalletNo("IP-ACY2-20260907-597057-P001");
|
||||
targetPallet.setWarehousePalletId(null);
|
||||
targetPallet.setWarehouseLocationId(null);
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
targetPallet.setAssignmentStatus("PENDING_ASSIGNMENT");
|
||||
WarehouseResourceGateway.WarehousePalletReference resource =
|
||||
new WarehouseResourceGateway.WarehousePalletReference(188L, "A002", 88L, 288L,
|
||||
"IDLE", "ACY2", 1);
|
||||
when(warehouse.reserve("100", 88L, 188L, 288L, "INBOUND_CONTAINER_DESTINATION_PALLET",
|
||||
"CONT-1:WH-A", "INBOUND_CONTAINER_DESTINATION_PALLET:CONT-1:WH-A:188")).thenReturn(999L);
|
||||
|
||||
service.assignResolvedPallet("100", plan, targetPallet, resource);
|
||||
|
||||
assertEquals("A002", targetPallet.getPalletNo());
|
||||
assertEquals(188L, targetPallet.getWarehousePalletId());
|
||||
assertEquals(288L, targetPallet.getWarehouseLocationId());
|
||||
verify(repository).update(targetPallet);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedAssignmentRejectsPhysicalPalletWithoutBusinessCodeBeforeReservation() {
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
WarehouseResourceGateway.WarehousePalletReference resource =
|
||||
new WarehouseResourceGateway.WarehousePalletReference(188L, " ", 88L, 288L,
|
||||
"IDLE", "ACY2", 1);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.assignResolvedPallet("100", plan, targetPallet, resource));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonScanBindsOnlyExpectedCartonAndRemoveMakesItScannableAgain() {
|
||||
InboundPlanCarton carton = carton("BOX-001", "UNASSIGNED", targetLine.getId(), "WH-A");
|
||||
@@ -522,6 +557,8 @@ class InboundPdaWorkflowServiceTest {
|
||||
@Test
|
||||
void dropshipTaskIsMarkedAndWholePalletPickReceivesThePlannedPalletAtOnce() {
|
||||
plan.setSourceOrderId(101L);
|
||||
targetPallet.setPalletNo("WH-PALLET-21");
|
||||
targetPallet.setSourcePalletNo("PLT-21");
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(repository.order(101L)).thenReturn(order);
|
||||
|
||||
+19
-2
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
|
||||
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.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
@@ -50,6 +51,7 @@ class InboundStockReceiptServiceTest {
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock ReceivingSkuReceiptMapper receivingSkuReceipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
private InboundStockReceiptService service;
|
||||
@@ -62,6 +64,12 @@ class InboundStockReceiptServiceTest {
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "receivingSkuReceipts", receivingSkuReceipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.resolveLocation(eq(900L), anyLong())).thenAnswer(invocation -> {
|
||||
long locationId = invocation.getArgument(1);
|
||||
return new WarehouseResourceGateway.WarehouseLocationReference(locationId,
|
||||
"PDA-LAX-" + locationId, "STORAGE", BigDecimal.TEN, "OCCUPIED");
|
||||
});
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
when(receipts.listTask("100", 2L)).thenReturn(List.of());
|
||||
@@ -86,11 +94,15 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals("SKU-1", request.getValue().skuCode());
|
||||
assertEquals("Customer", request.getValue().customerName());
|
||||
assertEquals("900", request.getValue().overseasWarehouseId());
|
||||
assertEquals("US-LAX-01", request.getValue().overseasWarehouseCode());
|
||||
assertEquals("Los Angeles", request.getValue().overseasWarehouseName());
|
||||
assertEquals("PDA-LAX-3001", request.getValue().warehouseLocationCode());
|
||||
assertEquals("PLT-21", request.getValue().palletNo());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
assertEquals(10L, receipt.getValue().getInventoryPositionId());
|
||||
assertEquals("PLT-21", receipt.getValue().getPalletNo());
|
||||
assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus());
|
||||
assertEquals("INBOUND_PLAN:COMPLETED", fixture.order.getSubOrderStatus());
|
||||
ArgumentCaptor<LastlegOrderStatusHistory> history = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class);
|
||||
@@ -216,6 +228,7 @@ class InboundStockReceiptServiceTest {
|
||||
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
fixture.pallet.setPalletNo("WH-PALLET-21");
|
||||
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
@@ -228,6 +241,8 @@ class InboundStockReceiptServiceTest {
|
||||
verify(dropshipOrders).activateAfterStockIn(eq("100"), eq(101L), eq(2L), activationReceipts.capture());
|
||||
assertEquals(1, activationReceipts.getValue().size());
|
||||
assertEquals(10L, activationReceipts.getValue().get(0).getInventoryPositionId());
|
||||
assertEquals("PLT-21", activationReceipts.getValue().get(0).getSourcePalletNo());
|
||||
assertEquals("WH-PALLET-21", activationReceipts.getValue().get(0).getPalletNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -292,7 +307,8 @@ class InboundStockReceiptServiceTest {
|
||||
line.setRequiredPackageCount(required); line.setVolumeCbm(new BigDecimal("1.0"));
|
||||
LastlegOrder order = new LastlegOrder(); order.setId(101L); order.setCustomerId("100");
|
||||
order.setOrderNo("LMO-1"); order.setCustomerName("Customer"); order.setSourceWarehouseId("100");
|
||||
order.setOverseasWarehouseId("900"); order.setOverseasWarehouseName("Los Angeles");
|
||||
order.setOverseasWarehouseId("900"); order.setOverseasWarehouseCode("US-LAX-01");
|
||||
order.setOverseasWarehouseName("Los Angeles");
|
||||
order.setSourceEventId("evt-1"); order.setFbaWarehouseCode("ONT8");
|
||||
order.setCurrentStatus("INBOUND_PLANNED");
|
||||
org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order);
|
||||
@@ -309,7 +325,8 @@ class InboundStockReceiptServiceTest {
|
||||
|
||||
private InboundPlanPallet pallet(long id, long warehousePalletId) {
|
||||
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(id); pallet.setPlanId(1L);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setSourcePalletNo("PLT-" + id);
|
||||
pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setWarehouseLocationId(3001L); return pallet;
|
||||
}
|
||||
|
||||
|
||||
+68
-6
@@ -112,9 +112,6 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
when(sessions.insert(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
when(plans.claimReceivingWarehouse("100", 2L, 100L)).thenReturn(1);
|
||||
InboundPlan claimed = plan("CONT-A"); claimed.setOverseasWarehouseId(100L);
|
||||
when(plans.selectById(2L)).thenReturn(claimed);
|
||||
var response = service.prepare("7", "receiver", 100L, "CONT-A",
|
||||
new ReceivingContainerRequests.Action("same-key"));
|
||||
assertThat(response.overseasWarehouseId()).isEqualTo(100L);
|
||||
@@ -179,6 +176,27 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
.containsExactly("CONT-EARLY", "CONT-LATE", "CONT-NULL", "CONT-BLOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidWarehouseSnapshotOnlyBlocksItsContainerWithoutFailingHealthyCandidates() {
|
||||
InboundPlan brokenPlan = plan("CONT-BROKEN");
|
||||
InboundPlan healthyPlan = plan("CONT-HEALTHY"); healthyPlan.setId(3L);
|
||||
InboundPlanLine brokenLine = line("FBA");
|
||||
InboundPlanLine healthyLine = line("FBA"); healthyLine.setId(11L); healthyLine.setOrderId(2L); healthyLine.setPlanId(3L);
|
||||
LastlegOrder broken = containerOrder(1L, "100", "CONT-BROKEN", "BOOKING-A", "IN-1");
|
||||
broken.setOverseasWarehouseCode(null);
|
||||
LastlegOrder healthy = containerOrder(2L, "100", "CONT-HEALTHY", "BOOKING-B", "IN-2");
|
||||
when(plans.selectList(any())).thenReturn(List.of(brokenPlan, healthyPlan));
|
||||
when(lines.selectList(any())).thenReturn(List.of(brokenLine, healthyLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(broken, healthy));
|
||||
|
||||
var result = service.candidates("7", 100L);
|
||||
|
||||
assertThat(result).extracting("containerNo").containsExactly("CONT-HEALTHY", "CONT-BROKEN");
|
||||
assertThat(result.get(0).operable()).isTrue();
|
||||
assertThat(result.get(1).operable()).isFalse();
|
||||
assertThat(result.get(1).blockReason()).isEqualTo("柜内接柜仓业务快照不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesIncludeUnassignedAndSameWarehouseButHideOtherWarehouse() {
|
||||
InboundPlan unassigned = plan("CONT-A");
|
||||
@@ -198,6 +216,47 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
.containsExactly("CONT-A", "CONT-B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleCandidateWithUnassignedPlanCanOpenDetailInTheSameWarehouse() {
|
||||
InboundPlan unassigned = plan("TSTU0000204");
|
||||
unassigned.setOverseasWarehouseId(null);
|
||||
InboundPlanLine planLine = line("FBA");
|
||||
LastlegOrder order = containerOrder(1L, "100", "TSTU0000204", "BOOKING-204", "IN-204");
|
||||
when(plans.selectList(any())).thenReturn(List.of(unassigned));
|
||||
when(lines.selectList(any())).thenReturn(List.of(planLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
when(orders.selectById(1L)).thenReturn(order);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
|
||||
var candidate = service.candidates("7", 100L).get(0);
|
||||
var detail = service.detail("7", 100L, "TSTU0000204");
|
||||
|
||||
assertThat(candidate.operable()).isTrue();
|
||||
assertThat(detail.overseasWarehouseId()).isEqualTo(100L);
|
||||
assertThat(detail.overseasWarehouseCode()).isEqualTo("SHARED-LAX");
|
||||
}
|
||||
|
||||
@Test
|
||||
void prepareAtomicallyBindsUnassignedPlansToThePickerWarehouse() {
|
||||
InboundPlan unassigned = plan("TSTU0195472");
|
||||
unassigned.setOverseasWarehouseId(null);
|
||||
InboundPlanLine planLine = line("FBA");
|
||||
LastlegOrder order = containerOrder(1L, "100", "TSTU0195472", "BOOKING-472", "IN-472");
|
||||
when(plans.selectList(any())).thenReturn(List.of(unassigned));
|
||||
when(plans.updateById(unassigned)).thenReturn(1);
|
||||
when(lines.selectList(any())).thenReturn(List.of(planLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
when(orders.selectById(1L)).thenReturn(order);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
|
||||
var result = service.prepare("7", "receiver", 100L, "TSTU0195472",
|
||||
new ReceivingContainerRequests.Action("bind-warehouse"));
|
||||
|
||||
assertThat(result.overseasWarehouseId()).isEqualTo(100L);
|
||||
assertThat(unassigned.getOverseasWarehouseId()).isEqualTo(100L);
|
||||
verify(plans).updateById(unassigned);
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesHideContainersReservedByAnotherOperator() {
|
||||
InboundPlan available = plan("CONT-A");
|
||||
@@ -652,7 +711,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static InboundPlan plan(String containerNo) {
|
||||
InboundPlan value = new InboundPlan();
|
||||
value.setId(2L); value.setPlanNo("PLAN-1"); value.setCustomerId("100"); value.setContainerNo(containerNo);
|
||||
value.setStatus("SIGNED");
|
||||
value.setStatus("SIGNED"); value.setOverseasWarehouseId(100L);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -668,7 +727,9 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static LastlegOrder order(String destinationType) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(1L); value.setCustomerId("100"); value.setDestinationType(destinationType);
|
||||
value.setOverseasWarehouseId("100"); value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
value.setOverseasWarehouseId("100"); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -690,7 +751,8 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static ReceivingContainerSession session(String containerNo, String status) {
|
||||
ReceivingContainerSession value = new ReceivingContainerSession();
|
||||
value.setId(8L); value.setCustomerId("100"); value.setContainerNo(containerNo); value.setOperatorKey("7");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setStatus(status); value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
+9
-3
@@ -48,9 +48,9 @@ class InboundPlanPaginationTest {
|
||||
null, "PENDING_ASSIGNMENT", null, null, "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
|
||||
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
|
||||
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
|
||||
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0));
|
||||
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
|
||||
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null,
|
||||
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
|
||||
when(queryMapper.count("100", query)).thenReturn(11L);
|
||||
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
|
||||
|
||||
@@ -59,6 +59,8 @@ class InboundPlanPaginationTest {
|
||||
assertThat(result.records()).containsExactly(row);
|
||||
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
|
||||
assertThat(result.records().get(0).appointmentStatus()).isNull();
|
||||
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
|
||||
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
|
||||
assertThat(result.pages()).isEqualTo(2);
|
||||
verify(queryMapper).count("100", query);
|
||||
verify(queryMapper).rows("100", query, 10, 10);
|
||||
@@ -71,6 +73,10 @@ class InboundPlanPaginationTest {
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
|
||||
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
|
||||
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
|
||||
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
|
||||
"o.overseas_warehouse_name overseasWarehouseName",
|
||||
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
|
||||
"q.destinationWarehouseCode", "q.warehouse",
|
||||
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
|
||||
|
||||
+213
-10
@@ -3,6 +3,7 @@ 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.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
@@ -15,6 +16,9 @@ import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -28,11 +32,13 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -47,6 +53,8 @@ class InboundPlanServiceTest {
|
||||
@Mock InboundPlanOrderMapper orderMapper;
|
||||
@Mock InventoryBalanceMapper inventoryBalanceMapper;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
@Mock LastlegOrderCommandRecordMapper commandMapper;
|
||||
|
||||
private InboundPlanService service;
|
||||
|
||||
@@ -60,10 +68,22 @@ class InboundPlanServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
||||
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
ReflectionTestUtils.setField(service, "commandMapper", commandMapper);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(orderMapper.findReceivingWarehouse(any(), any())).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
org.mockito.Mockito.lenient().when(orderMapper.lockContainerWarehouseFacts(any(), any())).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef(
|
||||
"100", 2L, "BOOKING-1", "193000000000200003")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,18 +119,107 @@ class InboundPlanServiceTest {
|
||||
.filter(item -> "PENDING_COUNT".equals(item.getStatus())).count());
|
||||
verify(orderMapper).markPlanned(any(), org.mockito.ArgumentMatchers.eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper).bindInboundPlan(org.mockito.ArgumentMatchers.eq("100"),
|
||||
org.mockito.ArgumentMatchers.eq("193000000000200003"),
|
||||
org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.eq("RK-2"),
|
||||
org.mockito.ArgumentMatchers.eq(result.planNo()), any());
|
||||
verify(orderMapper).insertHistory(any(), org.mockito.ArgumentMatchers.eq(2L), any(), any(), any());
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
assertEquals("MSCU6639870", result.containerNo());
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyCreatesOneIndependentPlanWithFrozenMetadata() {
|
||||
SourceOrderSnapshot source = order(2L, "RK-2", "1.0000", "WH-A");
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
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, "优先收货");
|
||||
|
||||
var result = service.convertManual(request, "manual-2", "900");
|
||||
|
||||
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("STANDARD", plan.getValue().getPalletType());
|
||||
assertEquals("MANUAL", plan.getValue().getCreationMode());
|
||||
assertEquals(2, result.palletCount());
|
||||
verify(palletMapper, times(2)).insert(any(InboundPlanPallet.class));
|
||||
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any());
|
||||
verify(commandMapper).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
verify(orderMapper, never()).lockContainerOrders(any());
|
||||
verify(orderMapper).lockContainerWarehouseFacts("100", 2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyCreatesPlanWhenStableWarehouseIdExistsWithoutDisplaySnapshots() {
|
||||
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200003", null, null, null,
|
||||
"WH-A", "WH-A", "仓A", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(3), eq(false), eq(0), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
|
||||
LocalDate.of(2026, 9, 15), 3, "STANDARD", true, false, false, null),
|
||||
"manual-id-only", "900");
|
||||
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
verify(warehouseGateway).requireActiveSharedWarehouse(193000000000200003L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanRejectsMixedContainerWarehousesBeforeLockingOrderOrWriting() {
|
||||
when(orderMapper.lockContainerWarehouseFacts("100", 2L)).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef("100", 2L, "BOOKING-1", "193000000000200003"),
|
||||
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),
|
||||
1, "STANDARD", false, false, false, null), "mixed-warehouse", "900"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesManualPlanForHeldOrderBeforeWriting() {
|
||||
SourceOrderSnapshot held = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, "WH-A", "仓A", 1, 0, true, 5);
|
||||
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),
|
||||
1, "STANDARD", false, false, false, null),
|
||||
"manual-held", "900"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsAConsistentDestinationAssignedOrder() {
|
||||
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10,
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"2090000000000000001", "LAX-02", "Los Angeles 02", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(assigned));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
@@ -145,7 +254,35 @@ class InboundPlanServiceTest {
|
||||
assertEquals(1L, result.sourceOrderId());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsReceivingWarehouseFromTenantScopedOrderFact() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot("88", "US-NY-01", "纽约仓"));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(88L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(88L, "US-NY-01", "纽约仓", "ACTIVE"));
|
||||
|
||||
var result = service.detailByPlanNo("100", "LIP-1");
|
||||
|
||||
assertEquals("US-NY-01", result.overseasWarehouseCode());
|
||||
assertEquals("纽约仓", result.overseasWarehouseName());
|
||||
verify(orderMapper).findReceivingWarehouse("100", 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailFailsClosedWhenReceivingWarehouseFactIsMissing() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.detailByPlanNo("100", "LIP-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,7 +297,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
@@ -190,7 +329,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void transferWarehouseOverridesOriginalCargoDestination() {
|
||||
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
@@ -217,6 +358,8 @@ class InboundPlanServiceTest {
|
||||
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getSourcePalletNo).toList());
|
||||
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||
@@ -318,6 +461,8 @@ class InboundPlanServiceTest {
|
||||
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getSourcePalletNo).toList());
|
||||
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||
@@ -332,13 +477,35 @@ class InboundPlanServiceTest {
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0)));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerConversionRejectsMixedWarehouseSnapshotsBeforeCreatingAnyPlan() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "200", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200004", null, null, null, "WH-B", "WH-B", "仓B", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(differentWarehouse));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -358,16 +525,50 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||
void partialExistingPlansArePreservedWhileOnlyRemainingOrdersAreCreated() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(order("100", 2L, "RK-2", "WH-B")));
|
||||
when(orderMapper.markPlanned(eq("100"), eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
var result = service.convertContainer("BOOKING-1");
|
||||
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("LIP-1", result.get(0).planNo());
|
||||
assertEquals(2L, result.get(1).sourceOrderId());
|
||||
verify(orderMapper, never()).lockOrder("100", 1L);
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(eq("100"), eq(1L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(orderMapper).markPlanned(eq("100"), eq(2L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansWithMixedWarehouseFactsCauseZeroNewWrites() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200004", null, null, null, "WH-B", "WH-B", "仓B", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(differentWarehouse));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -419,12 +620,14 @@ class InboundPlanServiceTest {
|
||||
|
||||
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, customerId, "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
|
||||
BigDecimal.ONE, 10, 0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -9,6 +9,8 @@ import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -82,6 +84,22 @@ class InventorySchemaContractTest {
|
||||
assertFalse(sql.contains("sku_code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void balanceQueriesHideZeroRowsAndMutationsKeepWarehouseDimensionCanonical() throws Exception {
|
||||
assertTrue(InventoryQueryMapper.BALANCE_FILTER.contains("procurement_packages)>0"));
|
||||
assertTrue(InventoryQueryMapper.JOINED_BALANCE_FILTER.contains("b.procurement_packages)>0"));
|
||||
String bindSql = String.join(" ", com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper.class
|
||||
.getMethod("bindInboundPlan", String.class, String.class, Long.class, String.class,
|
||||
String.class, java.time.LocalDateTime.class)
|
||||
.getAnnotation(Update.class).value());
|
||||
assertTrue(bindSql.contains("overseas_warehouse_id=#{overseasWarehouseId}"));
|
||||
String deleteSql = String.join(" ", com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper.class
|
||||
.getMethod("deleteEmptyBalance", String.class, Long.class)
|
||||
.getAnnotation(Delete.class).value());
|
||||
assertTrue(deleteSql.contains("in_transit_packages=0"));
|
||||
assertTrue(deleteSql.contains("NOT EXISTS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void incrementalMigrationIsAdditiveAndRerunnable() throws IOException {
|
||||
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8)
|
||||
|
||||
+139
-1
@@ -2,6 +2,8 @@ package com.baiyue.lastleg.inventory.command;
|
||||
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse;
|
||||
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
@@ -17,6 +19,7 @@ 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.apache.ibatis.annotations.Select;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@@ -57,7 +60,7 @@ class InventoryCommandServiceTest {
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "commands", commands);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
lenient().when(operations.findIdempotency(any(), any())).thenReturn(null);
|
||||
lenient().when(commands.lockInboundTransitBalances(any(), any(), any())).thenReturn(List.of());
|
||||
}
|
||||
@@ -140,6 +143,7 @@ class InventoryCommandServiceTest {
|
||||
source.setInTransitPackages(12L);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(target);
|
||||
when(commands.lockInboundTransitBalances("100", 101L, "IN-CODE-1")).thenReturn(List.of(source));
|
||||
when(commands.deleteEmptyBalance("100", 201L)).thenReturn(1);
|
||||
|
||||
InventoryCommandResponse result = service.stockIn("100", "7",
|
||||
stockIn("IN-1", "stock-in-cross-warehouse", 12, 24));
|
||||
@@ -152,6 +156,40 @@ class InventoryCommandServiceTest {
|
||||
assertEquals(-12L, written.getAllValues().get(0).getDeltaPackages());
|
||||
assertEquals("STOCK_IN", written.getAllValues().get(1).getBusinessType());
|
||||
assertEquals("IN_TRANSIT", written.getAllValues().get(1).getStatusFrom());
|
||||
verify(commands).deleteEmptyBalance("100", 201L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingWarehouseChangeRebindsOnlyInTransitBalanceAndWritesZeroDeltaAudit() {
|
||||
InventoryBalance balance = balance(200L, 0);
|
||||
balance.setInTransitPackages(12L);
|
||||
when(commands.lockInboundBalances("100", 101L)).thenReturn(List.of(balance));
|
||||
when(balances.lockDimension("100", "900", "ONT8", 101L, "IN-CODE-1")).thenReturn(null);
|
||||
|
||||
service.rebindReceivingWarehouse("100", 101L, 900L, "US-NYC-01", "纽约仓",
|
||||
"admin", "change-1");
|
||||
|
||||
assertEquals("900", balance.getOverseasWarehouseId());
|
||||
assertEquals("US-NYC-01", balance.getOverseasWarehouseCode());
|
||||
assertEquals("纽约仓", balance.getOverseasWarehouseName());
|
||||
ArgumentCaptor<InventoryLedger> audit = ArgumentCaptor.forClass(InventoryLedger.class);
|
||||
verify(ledgers).insert(audit.capture());
|
||||
assertEquals("RECEIVING_WAREHOUSE_CHANGED", audit.getValue().getBusinessType());
|
||||
assertEquals(0L, audit.getValue().getDeltaPackages());
|
||||
assertEquals(12L, audit.getValue().getBeforePackages());
|
||||
assertEquals(12L, audit.getValue().getAfterPackages());
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingWarehouseChangeRejectsPhysicalInventoryBeforeAnySnapshotMutation() {
|
||||
InventoryBalance balance = balance(200L, 1);
|
||||
when(commands.lockInboundBalances("100", 101L)).thenReturn(List.of(balance));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.rebindReceivingWarehouse(
|
||||
"100", 101L, 900L, "US-NYC-01", "纽约仓", "admin", "change-physical"));
|
||||
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
verify(ledgers, never()).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -294,6 +332,97 @@ class InventoryCommandServiceTest {
|
||||
verify(operations).findIdempotency("200", "platform-hold-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresReturnedInventoryFromImmutableShipmentSourceIntoVerifiedTarget() throws Exception {
|
||||
assertTrue(InventoryCommandService.class.getMethod("restoreReturnedInventory", String.class, String.class,
|
||||
ReturnedInventoryRestoreRequest.class).getAnnotation(Transactional.class) != null);
|
||||
InventoryBalance balance = balance(200L, 2);
|
||||
balance.setOverseasWarehouseId("900");
|
||||
InventoryPosition source = position(201L, 200L, 0);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(source);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L);
|
||||
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(1L);
|
||||
when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
||||
400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0));
|
||||
when(warehouseGateway.resolveLocation(900L, 500L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseLocationReference(500L, "LOC-RETURN", "STORAGE", null, "ACTIVE"));
|
||||
|
||||
ReturnedInventoryRestoreResponse result = service.restoreReturnedInventory("100", "7", returnRequest(3));
|
||||
|
||||
assertEquals(2, result.beforePackages());
|
||||
assertEquals(3, result.restoredPackages());
|
||||
assertEquals(5, result.afterPackages());
|
||||
assertFalse(result.replayed());
|
||||
ArgumentCaptor<InventoryPosition> target = ArgumentCaptor.forClass(InventoryPosition.class);
|
||||
verify(positions).insert(target.capture());
|
||||
assertEquals("RETURN:EX-1", target.getValue().getBatchNo());
|
||||
assertEquals("LOC-RETURN", target.getValue().getWarehouseLocationCode());
|
||||
assertEquals(3L, target.getValue().getAvailablePackages());
|
||||
ArgumentCaptor<InventoryLedger> ledger = ArgumentCaptor.forClass(InventoryLedger.class);
|
||||
verify(ledgers).insert(ledger.capture());
|
||||
assertEquals("RETURN_STOCK_IN", ledger.getValue().getBusinessType());
|
||||
assertEquals("RETURN_SOURCE:301:201", ledger.getValue().getSourceEventId());
|
||||
assertEquals("OUT-1", ledger.getValue().getOutboundOrderNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnedInventoryReplayDoesNotMutateInventoryTwice() {
|
||||
InventoryBalance balance = balance(200L, 0);
|
||||
balance.setOverseasWarehouseId("900");
|
||||
InventoryPosition source = position(201L, 200L, 0);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(source);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L);
|
||||
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(0L);
|
||||
when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
||||
400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0));
|
||||
when(warehouseGateway.resolveLocation(900L, 500L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseLocationReference(500L, "LOC-RETURN", "STORAGE", null, "ACTIVE"));
|
||||
ReturnedInventoryRestoreRequest request = returnRequest(3);
|
||||
service.restoreReturnedInventory("100", "7", request);
|
||||
ArgumentCaptor<InventoryOperation> operation = ArgumentCaptor.forClass(InventoryOperation.class);
|
||||
verify(operations).insert(operation.capture());
|
||||
when(operations.findIdempotency("100", "return-1")).thenReturn(operation.getValue());
|
||||
|
||||
ReturnedInventoryRestoreResponse replay = service.restoreReturnedInventory("100", "7", request);
|
||||
|
||||
assertTrue(replay.replayed());
|
||||
verify(positions, times(1)).insert(any(InventoryPosition.class));
|
||||
verify(ledgers, times(1)).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReturnBeyondRemainingImmutableShipmentWithoutInventoryMutation() {
|
||||
InventoryBalance balance = balance(200L, 0);
|
||||
balance.setOverseasWarehouseId("900");
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position(201L, 200L, 0));
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(4L);
|
||||
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(2L);
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.restoreReturnedInventory("100", "7", returnRequest(3)));
|
||||
|
||||
verify(positions, never()).insert(any(InventoryPosition.class));
|
||||
verify(positions, never()).updateById(any(InventoryPosition.class));
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
verify(ledgers, never()).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnRestoreSqlIsBoundToImmutableShipLedgerAndSourceIdentity() throws Exception {
|
||||
String shippedSql = String.join(" ", InventoryCommandMapper.class.getMethod("shippedPackages",
|
||||
String.class, Long.class, String.class, String.class).getAnnotation(Select.class).value());
|
||||
assertTrue(shippedSql.contains("position_id=#{positionId}"));
|
||||
assertTrue(shippedSql.contains("outbound_order_no=#{outboundNo}"));
|
||||
assertTrue(shippedSql.contains("business_type='SHIP'"));
|
||||
String restoredSql = String.join(" ", InventoryCommandMapper.class.getMethod("restoredPackages",
|
||||
String.class, String.class, String.class).getAnnotation(Select.class).value());
|
||||
assertTrue(restoredSql.contains("source_event_id=#{sourceIdentity}"));
|
||||
assertTrue(restoredSql.contains("business_type='RETURN_STOCK_IN'"));
|
||||
}
|
||||
|
||||
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", "SKU-1", "Product 1", null,
|
||||
@@ -307,10 +436,19 @@ class InventoryCommandServiceTest {
|
||||
packages, "ORD-1", "EVT-1", key, null);
|
||||
}
|
||||
|
||||
private ReturnedInventoryRestoreRequest returnRequest(long packages) {
|
||||
return new ReturnedInventoryRestoreRequest("EX-1", "OUT-1", "900", 101L,
|
||||
List.of(new ReturnedInventoryRestoreRequest.SourceItem(301L, 201L, "SKU-1", 5)),
|
||||
List.of(new ReturnedInventoryRestoreRequest.Receipt(301L, 201L, packages,
|
||||
400L, "PLT-RETURN", 500L, "LOC-RETURN")),
|
||||
"return-1", List.of("FILE-1"));
|
||||
}
|
||||
|
||||
private InventoryBalance balance(Long id, long available) {
|
||||
InventoryBalance value = new InventoryBalance();
|
||||
value.setId(id); value.setCustomerId("100"); value.setCustomerName("Customer 100");
|
||||
value.setOverseasWarehouseId("WH-1");
|
||||
value.setOverseasWarehouseCode("WH1"); value.setOverseasWarehouseName("Warehouse 1");
|
||||
value.setDestinationWarehouseCode("ONT8"); value.setInboundOrderId(101L);
|
||||
value.setInboundOrderNo("IN-1"); value.setInboundCode("IN-CODE-1");
|
||||
value.setAvailablePackages(available);
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.lastleg.inventory.query;
|
||||
|
||||
import com.baiyue.common.web.LongIdJacksonConfiguration;
|
||||
import com.baiyue.lastleg.inventory.query.controller.InventoryQueryController;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InventoryBalanceView;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InventoryBalanceDetailContractTest {
|
||||
private static final long BALANCE_ID = 19_000_000_000_000_001L;
|
||||
private static final long INBOUND_ORDER_ID = 19_000_000_000_000_002L;
|
||||
|
||||
@Test
|
||||
void controllerFreezesDetailPathAndInventoryListPermission() throws Exception {
|
||||
var method = InventoryQueryController.class.getMethod("balance",
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class);
|
||||
GetMapping mapping = method.getAnnotation(GetMapping.class);
|
||||
PreAuthorize permission = method.getAnnotation(PreAuthorize.class);
|
||||
|
||||
assertNotNull(mapping);
|
||||
assertEquals("/api/platform/v1/last-leg/inventory/balances/{balanceId}", mapping.value()[0]);
|
||||
assertNotNull(permission);
|
||||
assertTrue(permission.value().contains(OverseasPermissions.INVENTORY_LIST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiDefinesDirectDetailResponseAndStringLongIds() throws Exception {
|
||||
String yaml;
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("openapi/overseas-business-v1.yaml")) {
|
||||
assertNotNull(stream);
|
||||
yaml = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
assertTrue(yaml.contains("/api/platform/v1/last-leg/inventory/balances/{balanceId}:"));
|
||||
assertTrue(yaml.contains("$ref: '#/components/schemas/InventoryBalanceApiResponse'"));
|
||||
assertTrue(yaml.contains("id: { type: string, pattern: '^[1-9][0-9]*$'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void balanceAndInboundOrderIdsSerializeAsJsonStrings() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
|
||||
JacksonAutoConfiguration.class, LongIdJacksonConfiguration.class)).run(context -> {
|
||||
ObjectMapper mapper = context.getBean(ObjectMapper.class);
|
||||
InventoryBalanceView view = new InventoryBalanceView(BALANCE_ID, "100", "Customer", "900",
|
||||
"US-LAX-01", "Los Angeles", "800", "ONT8", "ONT8", INBOUND_ORDER_ID,
|
||||
"IBP-1", "RK-1", "SKU-1", "产品", "Product", 3, 3, 0, 0, 0, 0, 0, 0, null);
|
||||
|
||||
JsonNode json = mapper.readTree(mapper.writeValueAsString(view));
|
||||
|
||||
assertEquals(Long.toString(BALANCE_ID), json.path("id").textValue());
|
||||
assertEquals(Long.toString(INBOUND_ORDER_ID), json.path("inboundOrderId").textValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
+14
@@ -34,6 +34,20 @@ class InventoryQueryControllerTest {
|
||||
verify(service).balances(eq("10001"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformBalanceDetailQueriesByIdWithoutPaging() {
|
||||
controller.balance(user(0L, "PLATFORM"), 9007199254740993L, null);
|
||||
|
||||
verify(service).balance(null, 9007199254740993L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformBalanceDetailUsesExplicitCustomerScope() {
|
||||
controller.balance(user(0L, "PLATFORM"), 99L, " 10001 ");
|
||||
|
||||
verify(service).balance("10001", 99L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotUsePlatformInventoryController() {
|
||||
assertThrows(ResponseStatusException.class,
|
||||
|
||||
+27
@@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -115,6 +116,32 @@ class InventoryQueryServiceTest {
|
||||
verify(mapper).countBalances(null, query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformBalanceDetailReadsOneVisibleBalanceWithoutPaging() {
|
||||
InventoryBalance balance = new InventoryBalance();
|
||||
balance.setId(99L);
|
||||
balance.setCustomerId("200");
|
||||
balance.setAvailablePackages(3L);
|
||||
when(mapper.visibleBalance(99L)).thenReturn(balance);
|
||||
|
||||
var result = service.balance(null, 99L);
|
||||
|
||||
assertEquals(99L, result.id());
|
||||
assertEquals("200", result.customerId());
|
||||
assertEquals(3L, result.totalPackages());
|
||||
verify(mapper).visibleBalance(99L);
|
||||
verify(mapper, never()).countBalances(any(), any());
|
||||
verify(mapper, never()).balances(any(), any(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void scopedBalanceDetailDoesNotLeakAnotherCustomer() {
|
||||
assertThrows(ResponseStatusException.class, () -> service.balance("100", 99L));
|
||||
|
||||
verify(mapper).ownedVisibleBalance("100", 99L);
|
||||
verify(mapper, never()).visibleBalance(99L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownStatusIsRejectedBeforeDatabaseQuery() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery("DAMAGED");
|
||||
|
||||
+2
@@ -87,6 +87,8 @@ class LocalLooseboardServiceTest {
|
||||
assertEquals("SHARED_TRUCK", detail.summary().transportMode());
|
||||
verify(lastlegOrders).insert(org.mockito.ArgumentMatchers.<LastlegOrder>argThat(value ->
|
||||
"LOCAL_LOOSEBOARD".equals(value.getOrderType())
|
||||
&& value.getSourceOrderNo().equals("LB-WH-20260901-000001")
|
||||
&& value.getInboundCode() == null
|
||||
&& "LOCAL_LOOSEBOARD:PENDING_ACCEPTANCE".equals(value.getSubOrderStatus())));
|
||||
verify(histories).insert(org.mockito.ArgumentMatchers.<LocalLooseboardHistory>argThat(value ->
|
||||
"CREATE".equals(value.getOperationType())));
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LastlegOrderWarehouseQueryContractTest {
|
||||
@Test
|
||||
void warehouseFilterUsesOnlyFrozenReceivingWarehouseCodeAndName() {
|
||||
String sql = LastlegOrderMapper.WAREHOUSE_FILTER_SQL;
|
||||
|
||||
assertTrue(sql.contains("LOWER(TRIM(overseas_warehouse_code))"));
|
||||
assertTrue(sql.contains("LOWER(TRIM(overseas_warehouse_name))"));
|
||||
assertTrue(sql.contains("LIKE CONCAT('%',LOWER({0}),'%')"));
|
||||
assertFalse(sql.contains("overseas_warehouse_id"));
|
||||
assertFalse(sql.contains("transfer_warehouse"));
|
||||
}
|
||||
}
|
||||
+6
@@ -35,8 +35,14 @@ class MerchantOverseasOpenApiContractTest {
|
||||
.contains("#/components/schemas/TruckDetailResponse")
|
||||
.contains("#/components/schemas/LooseboardDetailResponse")
|
||||
.contains("#/components/schemas/InboundPlanDetailResponse")
|
||||
.contains("overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }")
|
||||
.contains("overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }")
|
||||
.contains("PENDING_REVIEW").contains("PENDING_ACCEPTANCE")
|
||||
.contains("version 必填").contains("幂等冲突");
|
||||
assertThat(yaml).contains("/api/platform/v1/last-leg/inbound-plans:")
|
||||
.contains("/api/platform/v1/last-leg/inbound-plans/{planNo}:")
|
||||
.contains("overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实")
|
||||
.contains("客户范围只取认证 Token,不接收 customerId");
|
||||
}
|
||||
|
||||
private static String pathBlock(String yaml, String path) {
|
||||
|
||||
+3
-1
@@ -74,13 +74,15 @@ class PdaOperationsOpenApiContractTest {
|
||||
"/api/pda/v1/last-leg/dispatches/{dispatchNo}/complete:",
|
||||
"/api/pda/v1/last-leg/dispatches/{dispatchNo}/depart:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks/resolve/{businessNo}:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/pick:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/complete:");
|
||||
for (String path : paths) assertTrue(yaml.contains(path), path);
|
||||
assertEquals(32, paths.size());
|
||||
assertEquals(33, paths.size());
|
||||
assertTrue(yaml.contains("enum: [LOCATION, PALLET]"));
|
||||
assertTrue(yaml.contains("enum: [APPROVE, REWORK]"));
|
||||
assertTrue(yaml.contains("Warehouse 权威实物托盘码"));
|
||||
assertTrue(yaml.contains("enum: [DAMAGED, MISSING_LABEL, LABEL_MISMATCH, UNREADABLE, OTHER]"));
|
||||
assertEquals(3, occurrences(yaml, "body.code=A0404"));
|
||||
assertFalse(yaml.contains("/api/pda/v1/last-leg/delivery"));
|
||||
|
||||
@@ -28,6 +28,8 @@ class RelabelContractTest {
|
||||
void schemasDefineRelabelApplicationItemsAndHistoryContracts() throws Exception {
|
||||
String baseline = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8).toLowerCase();
|
||||
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8).toLowerCase();
|
||||
String deployment = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8).toLowerCase();
|
||||
for (String schema : new String[] { baseline, migration }) {
|
||||
assertTrue(schema.contains("ll_relabel_application"));
|
||||
assertTrue(schema.contains("ll_relabel_item"));
|
||||
@@ -36,6 +38,13 @@ class RelabelContractTest {
|
||||
assertTrue(schema.contains("idempotency_key"));
|
||||
assertTrue(schema.contains("request_hash"));
|
||||
}
|
||||
for (String schema : new String[] { baseline, migration, deployment }) {
|
||||
assertTrue(schema.contains("ll_relabel_item"));
|
||||
assertTrue(schema.contains("remark"));
|
||||
assertTrue(schema.contains("carton_mark"));
|
||||
assertTrue(schema.contains("fba_carton_mark"));
|
||||
assertTrue(schema.contains("destination_warehouse_id"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-3
@@ -38,6 +38,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -119,9 +120,9 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
|
||||
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
|
||||
assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
|
||||
assertEquals(tailOrder.getValue().getInboundCode() + ":WH-1", tailOrder.getValue().getSourceInboundId());
|
||||
assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo());
|
||||
assertTrue(tailOrder.getValue().getInboundCode().startsWith("RL-"));
|
||||
assertNull(tailOrder.getValue().getInboundCode());
|
||||
assertEquals(application.getValue().getRelabelNo() + ":WH-1", tailOrder.getValue().getSourceInboundId());
|
||||
assertEquals(tailOrder.getValue().getRelabelOrderNo(), application.getValue().getRelabelNo());
|
||||
assertEquals(10, tailOrder.getValue().getPackageCount());
|
||||
assertEquals(6, application.getValue().getNotifiedPackageCount());
|
||||
assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
|
||||
|
||||
+8
-1
@@ -17,7 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class SchemaChineseCommentContractTest {
|
||||
|
||||
private static final Set<String> MODULE_BASELINE_ONLY_TABLES = Set.of(
|
||||
"ll_us_truck_order_operation", "ll_local_looseboard_operation");
|
||||
"ll_us_truck_order_operation", "ll_local_looseboard_operation",
|
||||
"ll_receiving_warehouse_operation");
|
||||
private static final Set<String> MODULE_BASELINE_ONLY_COLUMNS = Set.of(
|
||||
"ll_order.overseas_warehouse_code", "ll_order.receiving_warehouse_version",
|
||||
"ll_receiving_container_session.overseas_warehouse_code");
|
||||
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
@@ -138,6 +142,9 @@ class SchemaChineseCommentContractTest {
|
||||
continue;
|
||||
}
|
||||
for (String column : table.columns()) {
|
||||
if (MODULE_BASELINE_ONLY_COLUMNS.contains(table.name() + "." + column)) {
|
||||
continue;
|
||||
}
|
||||
boolean repaired = migration.contains("table_name='" + table.name()
|
||||
+ "' AND column_name='" + column + "'");
|
||||
boolean addedWithComment = hasCommentedAdd(migration, column)
|
||||
|
||||
+10
@@ -24,6 +24,11 @@ class Phase5AdminPermissionContractTest {
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("page"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("resolve"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_CREATE), permissions.get("create"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("detail"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("uploadEvidence"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("startProcessing"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("complete"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("cancel"));
|
||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("returnOptions"));
|
||||
assertEquals(authority(OverseasPermissions.DELIVERY_RETURN), permissions.get("markReturn"));
|
||||
}
|
||||
@@ -49,6 +54,7 @@ class Phase5AdminPermissionContractTest {
|
||||
OverseasPermissions.DISPATCH_LIST,
|
||||
OverseasPermissions.EXCEPTION_ORDER_LIST,
|
||||
OverseasPermissions.EXCEPTION_ORDER_CREATE,
|
||||
OverseasPermissions.EXCEPTION_ORDER_PROCESS,
|
||||
OverseasPermissions.DELIVERY_RETURN,
|
||||
OverseasPermissions.DELIVERY_STATUS,
|
||||
OverseasPermissions.CUSTOMER_VISIT_LIST,
|
||||
@@ -59,6 +65,10 @@ class Phase5AdminPermissionContractTest {
|
||||
for (String path : new String[] {
|
||||
"/api/platform/v1/last-leg/dispatches:",
|
||||
"/api/platform/v1/last-leg/exception-orders:",
|
||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}:",
|
||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/start-processing:",
|
||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/complete:",
|
||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/cancel:",
|
||||
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:",
|
||||
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:",
|
||||
"/api/platform/v1/last-leg/customer-visits:",
|
||||
|
||||
+14
-6
@@ -18,12 +18,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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 ContainerPlanProgressContractTest {
|
||||
@Test
|
||||
void responseFieldsAndServerAggregateStayFrozen() throws Exception {
|
||||
assertEquals(List.of("sourceContainerBookingId", "containerNo", "totalOrderCount", "inboundCodeCount",
|
||||
assertEquals(List.of("sourceContainerBookingId", "containerNo", "overseasWarehouseId",
|
||||
"overseasWarehouseCode", "overseasWarehouseName", "assignmentVersion",
|
||||
"canChangeReceivingWarehouse", "receivingWarehouseLockReason", "totalOrderCount", "inboundCodeCount",
|
||||
"missingInboundCodeCount", "planCount", "plannedInboundCodeCount", "pendingPlanCount",
|
||||
"plannedArrivalAt", "allPlansReady", "blockingReason"),
|
||||
Arrays.stream(ContainerPlanProgressSummary.class.getRecordComponents())
|
||||
@@ -38,8 +41,8 @@ class ContainerPlanProgressContractTest {
|
||||
String sql = String.join("", rows.value()).toLowerCase();
|
||||
assertTrue(sql.contains("left join ll_inbound_plan"));
|
||||
assertTrue(sql.contains("o.estimated_warehouse_at"));
|
||||
assertTrue(sql.contains("having count(distinct case when p.id is not null"));
|
||||
assertTrue(sql.contains("< count(distinct nullif(trim(o.inbound_code),''))"));
|
||||
assertTrue(sql.contains("group by o.source_container_booking_id"));
|
||||
assertFalse(sql.contains("having "));
|
||||
assertTrue(sql.contains("progress.plannedarrivalat asc"));
|
||||
assertTrue(sql.contains("progress.sourcecontainerbookingid asc"));
|
||||
assertTrue(sql.contains("limit #{limit} offset #{offset}"));
|
||||
@@ -56,7 +59,12 @@ class ContainerPlanProgressContractTest {
|
||||
assertTrue(openApi.contains("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}:"));
|
||||
assertTrue(openApi.contains("ContainerPlanProgressSummary:"));
|
||||
assertTrue(openApi.contains("allPlansReady:"));
|
||||
assertTrue(openApi.contains("仅返回仍有入仓码待生成有效独立计划的柜"));
|
||||
assertTrue(openApi.contains("返回全部稳定来源柜"));
|
||||
assertTrue(openApi.contains("全部入仓码均已生成有效独立计划的柜仍保留"));
|
||||
assertTrue(openApi.contains("权限 platform:overseas-warehouse:last-mile-order:list"));
|
||||
assertTrue(openApi.contains("权限 platform:overseas-warehouse:last-mile-order:edit"));
|
||||
assertFalse(openApi.contains("platform:overseas-warehouse:order:list"));
|
||||
assertFalse(openApi.contains("platform:overseas-warehouse:order:edit"));
|
||||
assertTrue(openApi.contains("ll_order.estimated_warehouse_at"));
|
||||
assertTrue(schema.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
assertTrue(deployment.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
@@ -75,8 +83,8 @@ class ContainerPlanProgressContractTest {
|
||||
|
||||
assertTrue(sql.contains("source_container_booking_id<>''"));
|
||||
assertTrue(sql.contains("LEFT JOIN ll_inbound_plan"));
|
||||
assertTrue(sql.contains("HAVING COUNT(DISTINCT CASE WHEN p.id IS NOT NULL"));
|
||||
assertTrue(sql.contains("< COUNT(DISTINCT NULLIF(TRIM(o.inbound_code),''))"));
|
||||
assertTrue(sql.contains("GROUP BY o.source_container_booking_id"));
|
||||
assertFalse(sql.contains("HAVING "));
|
||||
assertTrue(!sql.contains("<"));
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundOrderDetainedOutboxServiceTest {
|
||||
@Mock private LastlegEventOutboxMapper mapper;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
private InboundOrderDetainedOutboxService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundOrderDetainedOutboxService();
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void localSyntheticSourceReferenceIsNotWrittenAsEventCausationId() throws Exception {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(101L);
|
||||
order.setCustomerId("201");
|
||||
order.setOrderNo("WT-WH-20260907-972865");
|
||||
order.setSourceInboundId("WAREHOUSE_TRANSFER:WT-WH-20260907-778561");
|
||||
order.setInboundCode("NYEXX2609070253");
|
||||
order.setSourceEventId("WAREHOUSE_TRANSFER:WT-WH-20260907-778561");
|
||||
|
||||
service.publish(order, true, "等待质检", "301", "operator", Instant.parse("2026-09-07T16:00:00Z"), 1L);
|
||||
|
||||
ArgumentCaptor<LastlegEventOutbox> captor = ArgumentCaptor.forClass(LastlegEventOutbox.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
JsonNode envelope = objectMapper.readTree(captor.getValue().getPayloadJson());
|
||||
assertThat(envelope.path("causationId").isNull()).isTrue();
|
||||
assertThat(envelope.path("aggregateId").asText()).isEqualTo(order.getSourceInboundId());
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderCommandRecord;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderHoldHistory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderManualNodeHistory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderHoldHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LastlegOrderCommandServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderCommandRecordMapper commands;
|
||||
@Mock LastlegOrderHoldHistoryMapper holdHistory;
|
||||
@Mock LastlegOrderManualNodeMapper manualNodes;
|
||||
@Mock LastlegOrderManualNodeHistoryMapper manualNodeHistory;
|
||||
@Mock LastlegOrderOperationLogMapper operationLogs;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
LastlegOrderCommandService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new LastlegOrderCommandService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "commands", commands);
|
||||
ReflectionTestUtils.setField(service, "holdHistory", holdHistory);
|
||||
ReflectionTestUtils.setField(service, "manualNodes", manualNodes);
|
||||
ReflectionTestUtils.setField(service, "manualNodeHistory", manualNodeHistory);
|
||||
ReflectionTestUtils.setField(service, "operationLogs", operationLogs);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsOnlySelectedOrderWithVersionAndImmutableHistory() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
when(orders.updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3),
|
||||
any(LocalDateTime.class))).thenReturn(1);
|
||||
|
||||
var response = service.hold("100", 11L, "hold-11",
|
||||
new LastlegOrderCommandDtos.HoldRequest(true, true, "海关查验", 3), "900", "管理员");
|
||||
|
||||
assertTrue(response.held());
|
||||
assertTrue(response.chargeable());
|
||||
assertEquals(4, response.version());
|
||||
verify(orders).updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3), any());
|
||||
verify(holdHistory).insert(any(LastlegOrderHoldHistory.class));
|
||||
verify(commands).insert(any(LastlegOrderCommandRecord.class));
|
||||
verify(orders, never()).lockContainerOrders(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleHoldWithoutAnyWrite() {
|
||||
LastlegOrder order = order(11L, 4);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.hold("100", 11L,
|
||||
"hold-stale", new LastlegOrderCommandDtos.HoldRequest(true, false, "待核对", 3), "900", "管理员"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(orders, never()).updateHold(anyString(), anyLong(), anyBoolean(), anyBoolean(),
|
||||
nullable(String.class), anyInt(), any(LocalDateTime.class));
|
||||
verify(commands, never()).insert(any(LastlegOrderCommandRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesExactEightRowManualSnapshotInFrozenDisplayOrder() {
|
||||
LastlegOrder order = order(12L, 0);
|
||||
when(orders.selectById(12L)).thenReturn(order);
|
||||
when(orders.lockById("100", 12L)).thenReturn(order);
|
||||
when(orders.updateManualNodes(eq("100"), eq(12L), any(), any(), any(), eq(0), any())).thenReturn(1);
|
||||
LocalDateTime filled = LocalDateTime.of(2026, 9, 8, 10, 0);
|
||||
List<LastlegOrderCommandDtos.ManualNode> nodes = List.of(
|
||||
node("UNLOADED", null), node("PICKUP_APPOINTED", null),
|
||||
node("ACTUAL_ARRIVAL_WAITING_UNLOAD", null), node("OVERSEAS_CUSTOMS_COMPLETED", null),
|
||||
node("ESTIMATED_ARRIVAL", filled), node("ACTUAL_DEPARTURE", null),
|
||||
node("ESTIMATED_DEPARTURE", null), node("DOMESTIC_CUSTOMS_COMPLETED", null));
|
||||
|
||||
var response = service.saveManualNodes("100", 12L, "nodes-12",
|
||||
new LastlegOrderCommandDtos.ManualNodesRequest(0, nodes), "900", "管理员");
|
||||
|
||||
assertTrue(response.manualOverrideEnabled());
|
||||
assertEquals(8, response.nodes().size());
|
||||
assertEquals("DOMESTIC_CUSTOMS_COMPLETED", response.nodes().get(0).nodeCode());
|
||||
assertEquals("UNLOADED", response.nodes().get(7).nodeCode());
|
||||
ArgumentCaptor<LastlegOrderManualNode> rows = ArgumentCaptor.forClass(LastlegOrderManualNode.class);
|
||||
verify(manualNodes, org.mockito.Mockito.times(8)).insert(rows.capture());
|
||||
assertEquals(8, rows.getAllValues().size());
|
||||
verify(manualNodeHistory).insert(any(LastlegOrderManualNodeHistory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIncompleteOrFullyEmptyManualSnapshot() {
|
||||
LastlegOrder order = order(12L, 0);
|
||||
when(orders.selectById(12L)).thenReturn(order);
|
||||
when(orders.lockById("100", 12L)).thenReturn(order);
|
||||
List<LastlegOrderCommandDtos.ManualNode> nodes = List.of(
|
||||
node("DOMESTIC_CUSTOMS_COMPLETED", null), node("ESTIMATED_DEPARTURE", null),
|
||||
node("ACTUAL_DEPARTURE", null), node("ESTIMATED_ARRIVAL", null),
|
||||
node("OVERSEAS_CUSTOMS_COMPLETED", null), node("ACTUAL_ARRIVAL_WAITING_UNLOAD", null),
|
||||
node("PICKUP_APPOINTED", null), node("UNLOADED", null));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
|
||||
service.saveManualNodes("100", 12L, "nodes-empty",
|
||||
new LastlegOrderCommandDtos.ManualNodesRequest(0, nodes), "900", "管理员"));
|
||||
|
||||
assertEquals(400, error.getStatusCode().value());
|
||||
verify(orders, never()).updateManualNodes(anyString(), anyLong(), nullable(LocalDateTime.class),
|
||||
nullable(LocalDateTime.class), nullable(LocalDateTime.class), anyInt(), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
private LastlegOrderCommandDtos.ManualNode node(String code, LocalDateTime time) {
|
||||
return new LastlegOrderCommandDtos.ManualNode(code, time);
|
||||
}
|
||||
|
||||
private LastlegOrder order(long id, int version) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(id);
|
||||
order.setCustomerId("100");
|
||||
order.setOrderNo("LL-" + id);
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT");
|
||||
order.setDetained(false);
|
||||
order.setVersion(version);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
+184
-6
@@ -14,6 +14,9 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -28,6 +31,9 @@ import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -37,6 +43,7 @@ import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
@@ -48,10 +55,13 @@ class LastlegOrderServiceTest {
|
||||
@Mock LastlegOrderStatusHistoryMapper histories;
|
||||
@Mock LastlegOrderFeeMapper fees;
|
||||
@Mock LastlegOrderOperationLogMapper operations;
|
||||
@Mock LastlegOrderManualNodeMapper manualNodes;
|
||||
@Mock InboundPlanCartonMapper cartons;
|
||||
@Mock FbaWarehouseValidator fbaWarehouses;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
||||
LastlegOrderService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -62,6 +72,7 @@ class LastlegOrderServiceTest {
|
||||
ReflectionTestUtils.setField(service, "historyMapper", histories);
|
||||
ReflectionTestUtils.setField(service, "feeMapper", fees);
|
||||
ReflectionTestUtils.setField(service, "operationLogMapper", operations);
|
||||
ReflectionTestUtils.setField(service, "manualNodeMapper", manualNodes);
|
||||
ReflectionTestUtils.setField(service, "cartonMapper", cartons);
|
||||
ReflectionTestUtils.setField(service, "fbaWarehouseValidator", fbaWarehouses);
|
||||
lenient().when(fbaWarehouses.required(any(), any(), any())).thenAnswer(invocation ->
|
||||
@@ -70,6 +81,56 @@ class LastlegOrderServiceTest {
|
||||
"US", "CA"));
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
|
||||
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesReceivingWarehouseForAllPlansReadyContainerBeforePdaSession() {
|
||||
LastlegOrder first = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
||||
LastlegOrder second = order(2L, "200", "PENDING_ASSIGNMENT", null);
|
||||
for (LastlegOrder value : List.of(first, second)) {
|
||||
value.setReceivingWarehouseVersion(0); value.setOverseasWarehouseId("193000000000200003");
|
||||
value.setOverseasWarehouseCode("US-LAX-01"); value.setOverseasWarehouseName("洛杉矶仓");
|
||||
}
|
||||
var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900",
|
||||
"US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 2, 2, 0,
|
||||
null, true, null);
|
||||
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second));
|
||||
when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE"));
|
||||
when(orders.updateContainerReceivingWarehouse("BOOKING-1", "900", "US-NYC-01", "纽约仓", 0))
|
||||
.thenReturn(2);
|
||||
when(orders.findContainerProgress("BOOKING-1")).thenReturn(updated);
|
||||
|
||||
var result = service.changeReceivingWarehouse("BOOKING-1", "change-1",
|
||||
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, "改派纽约"), "admin");
|
||||
|
||||
assertEquals("US-NYC-01", result.overseasWarehouseCode());
|
||||
assertTrue(result.allPlansReady());
|
||||
assertTrue(result.canChangeReceivingWarehouse());
|
||||
verify(inventory).rebindReceivingWarehouse("100", 1L, 900L,
|
||||
"US-NYC-01", "纽约仓", "admin", "change-1");
|
||||
verify(inventory).rebindReceivingWarehouse("200", 2L, 900L,
|
||||
"US-NYC-01", "纽约仓", "admin", "change-1");
|
||||
verify(orders).updateContainerPlanReceivingWarehouse("BOOKING-1", 900L);
|
||||
verify(receivingWarehouseOperations).insert(any(com.baiyue.lastleg.entity.ReceivingWarehouseOperation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void permanentlyLocksReceivingWarehouseAfterPdaSessionExists() {
|
||||
LastlegOrder order = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
||||
order.setReceivingWarehouseVersion(0); order.setOverseasWarehouseId("193000000000200003");
|
||||
order.setOverseasWarehouseCode("US-LAX-01"); order.setOverseasWarehouseName("洛杉矶仓");
|
||||
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(order));
|
||||
when(orders.countReceivingSessions("BOOKING-1")).thenReturn(1L);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
|
||||
service.changeReceivingWarehouse("BOOKING-1", "change-2",
|
||||
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, null), "admin"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verifyNoInteractions(warehouseResourceGateway);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,6 +142,7 @@ class LastlegOrderServiceTest {
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(fees.selectList(any())).thenReturn(List.of());
|
||||
when(operations.selectList(any())).thenReturn(List.of());
|
||||
when(manualNodes.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var response = service.detail("100", 11L);
|
||||
|
||||
@@ -224,6 +286,26 @@ class LastlegOrderServiceTest {
|
||||
assertEquals(25, page.getValue().getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageWarehouseFilterMatchesFrozenWarehouseName() {
|
||||
assertWarehouseFilter("洛杉矶仓", "洛杉矶仓", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageWarehouseFilterMatchesFrozenWarehouseCodeIgnoringCaseAndSpaces() {
|
||||
assertWarehouseFilter(" us-lax-01 ", "us-lax-01", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageWarehouseFilterMatchesPartialFrozenWarehouseCode() {
|
||||
assertWarehouseFilter("LAX-0", "LAX-0", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageWarehouseFilterReturnsEmptyPageWhenNothingMatches() {
|
||||
assertWarehouseFilter("WAREHOUSE-NOT-FOUND", "WAREHOUSE-NOT-FOUND", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerProgressPageUsesServerSideAggregatePagination() {
|
||||
var query = new LastlegOrderRequests.ContainerProgressQuery(2, 10, "CONT-", null, null);
|
||||
@@ -269,20 +351,65 @@ class LastlegOrderServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerProgressDetailFailsClosedWhenContainerNumberFactHasDrifted() {
|
||||
void containerProgressDetailKeepsInvalidHistoricalContainerReadableAndBlocked() {
|
||||
var drifted = new ContainerPlanProgressSummary("BOOKING-1", null, 2, 2, 0,
|
||||
2, 2, 0, LocalDateTime.of(2026, 10, 3, 9, 0), false, "柜号缺失或不一致");
|
||||
when(orders.findContainerProgress("BOOKING-1")).thenReturn(drifted);
|
||||
when(orders.findContainerProgressItems("BOOKING-1")).thenReturn(List.of());
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.containerProgressDetail("BOOKING-1"));
|
||||
var result = service.containerProgressDetail("BOOKING-1");
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(orders, never()).findContainerProgressItems(any());
|
||||
assertFalse(result.summary().allPlansReady());
|
||||
assertFalse(result.summary().canChangeReceivingWarehouse());
|
||||
assertEquals("来源柜真实集装箱号缺失、格式无效或存在漂移",
|
||||
result.summary().blockingReason());
|
||||
assertEquals(result.summary().blockingReason(), result.summary().receivingWarehouseLockReason());
|
||||
verify(orders).findContainerProgressItems("BOOKING-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerProgressPageKeepsMissingWarehouseSnapshotFromBreakingHealthyContainers() {
|
||||
var query = new LastlegOrderRequests.ContainerProgressQuery(1, 10, null, null, null);
|
||||
var healthy = summary(false, 3, "存在3票尚未生成有效入库计划");
|
||||
var missingWarehouse = new ContainerPlanProgressSummary("BOOKING-2", "MSCU6639870",
|
||||
null, null, null, 0, true, null, 2, 2, 0, 0, 0, 2,
|
||||
LocalDateTime.of(2026, 10, 4, 9, 0), false, "存在2票尚未生成有效入库计划");
|
||||
when(orders.countContainerProgress(query)).thenReturn(2L);
|
||||
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(healthy, missingWarehouse));
|
||||
|
||||
var result = service.containerProgressPage(query);
|
||||
|
||||
assertEquals(2, result.records().size());
|
||||
assertSame(healthy, result.records().get(0));
|
||||
assertEquals("柜内接柜仓业务快照不一致", result.records().get(1).blockingReason());
|
||||
assertFalse(result.records().get(1).allPlansReady());
|
||||
assertTrue(result.records().get(1).canChangeReceivingWarehouse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerProgressPageKeepsReadyAndPdaLockedContainersVisible() {
|
||||
var query = new LastlegOrderRequests.ContainerProgressQuery(1, 10, null, null, null);
|
||||
var ready = summary(true, 0, null);
|
||||
var locked = new ContainerPlanProgressSummary("BOOKING-2", "TSTU0000061",
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", 4, false,
|
||||
"海外PDA已建立接柜会话,接柜仓已永久锁定", 1, 1, 0, 1, 1, 0,
|
||||
LocalDateTime.of(2026, 10, 4, 9, 0), true, null);
|
||||
when(orders.countContainerProgress(query)).thenReturn(2L);
|
||||
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(ready, locked));
|
||||
|
||||
var result = service.containerProgressPage(query);
|
||||
|
||||
assertEquals(List.of(ready, locked), result.records());
|
||||
assertTrue(result.records().get(0).allPlansReady());
|
||||
assertTrue(result.records().get(0).canChangeReceivingWarehouse());
|
||||
assertFalse(result.records().get(1).canChangeReceivingWarehouse());
|
||||
assertEquals("海外PDA已建立接柜会话,接柜仓已永久锁定",
|
||||
result.records().get(1).receivingWarehouseLockReason());
|
||||
}
|
||||
|
||||
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {
|
||||
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", 15, 15, 0,
|
||||
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003",
|
||||
"US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0,
|
||||
15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason);
|
||||
}
|
||||
|
||||
@@ -306,6 +433,31 @@ class LastlegOrderServiceTest {
|
||||
assertEquals(List.of("INBOUND-2", "INBOUND-3"), result.records().get(0).palletMergeInboundNos());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageDoesNotExposeBusinessOrderNumbersAsInboundCodes() {
|
||||
LastlegOrder relabel = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
||||
relabel.setOrderType("RELABEL_APPLICATION");
|
||||
relabel.setInboundCode("RL-WH-20260906-701570");
|
||||
LastlegOrder looseboard = order(12L, "100", "PENDING_ASSIGNMENT", null);
|
||||
looseboard.setOrderType("LOCAL_LOOSEBOARD");
|
||||
looseboard.setInboundCode("LB-WH-20260906-187457");
|
||||
LastlegOrder firstleg = order(13L, "100", "PENDING_ASSIGNMENT", null);
|
||||
firstleg.setOrderType("FIRST_LEG");
|
||||
firstleg.setInboundCode("NYEXX2609060063");
|
||||
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
||||
resultPage.setRecords(List.of(relabel, looseboard, firstleg));
|
||||
resultPage.setTotal(3);
|
||||
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
||||
var query = new LastlegOrderRequests.Query(1L, 10L, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
assertNull(result.records().get(0).inboundCode());
|
||||
assertNull(result.records().get(1).inboundCode());
|
||||
assertEquals("NYEXX2609060063", result.records().get(2).inboundCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
|
||||
@@ -352,4 +504,30 @@ class LastlegOrderServiceTest {
|
||||
return order;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void assertWarehouseFilter(String input, String normalized, boolean matched) {
|
||||
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
||||
LastlegOrder row = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
||||
row.setOverseasWarehouseCode("US-LAX-01");
|
||||
row.setOverseasWarehouseName("洛杉矶仓");
|
||||
resultPage.setRecords(matched ? List.of(row) : List.of());
|
||||
resultPage.setTotal(matched ? 1 : 0);
|
||||
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
||||
|
||||
var result = service.page(null, new LastlegOrderRequests.Query(1L, 10L, null, null, null, null,
|
||||
null, null, null, null, input, null, null, null, null, null));
|
||||
|
||||
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<LastlegOrder>> filter =
|
||||
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
||||
verify(orders).selectPage(any(), filter.capture());
|
||||
var wrapper = (com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<LastlegOrder>) filter.getValue();
|
||||
String warehouseSql = wrapper.getExpression().getNormal().getSqlSegment();
|
||||
assertTrue(warehouseSql.contains("LOWER(TRIM(overseas_warehouse_code))"));
|
||||
assertTrue(warehouseSql.contains("LOWER(TRIM(overseas_warehouse_name))"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().containsValue(normalized),
|
||||
wrapper.getParamNameValuePairs().toString());
|
||||
assertEquals(matched ? 1 : 0, result.total());
|
||||
assertEquals(matched ? 1 : 0, result.records().size());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-1
@@ -5,7 +5,7 @@ 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(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(3); 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())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
|
||||
@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)); }
|
||||
@Test void freezesFormalPlannedWarehouseArrivalFromLoadingEvent(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("estimatedWarehouseArrivalAt","2026-10-03T09:00"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=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); service.consume(changed); ArgumentCaptor<LastlegOrder> captor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captor.capture()); assertTrue(captor.getAllValues().stream().allMatch(order->java.time.LocalDateTime.of(2026,10,3,9,0).equals(order.getEstimatedWarehouseAt()))); }
|
||||
@@ -27,6 +27,12 @@ class LoadingCompletedConsumerTest {
|
||||
}
|
||||
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
|
||||
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","MSCU6639870","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals("123e4567-e89b-12d3-a456-426614174010:DROPSHIP:in-d",captor.getValue().idempotencyKey()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
|
||||
@Test void freezesReceivingWarehouseInInitialInTransitInventory(){
|
||||
service.consume(event());
|
||||
ArgumentCaptor<InventoryCommandRequests.InTransit> captured=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class);
|
||||
verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),captured.capture());
|
||||
assertTrue(captured.getAllValues().stream().allMatch(value->"193000000000200003".equals(value.overseasWarehouseId())&&"US-LAX-01".equals(value.overseasWarehouseCode())&&"洛杉矶仓".equals(value.overseasWarehouseName())));
|
||||
}
|
||||
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){
|
||||
var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2"));
|
||||
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3"));
|
||||
|
||||
Reference in New Issue
Block a user