This commit is contained in:
2026-09-08 18:16:54 +08:00
parent 1a1a19dea7
commit fcb4c055e2
32 changed files with 1239 additions and 46 deletions
@@ -0,0 +1,81 @@
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 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;
private CustomerVisitService service;
@BeforeEach
void setUp() {
service = new CustomerVisitService();
ReflectionTestUtils.setField(service, "visits", visits);
ReflectionTestUtils.setField(service, "deliveryOrders", deliveryOrders);
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
}
@Test
void rejectsRollbackFromDeliveredToWaitDeliveryWithoutWritingHistory() {
DeliveryOrder delivery = delivery("DELIVERED");
when(deliveryOrders.findByNo("1001", "DL-1")).thenReturn(delivery);
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("WAIT_DELIVERY", "错误倒退")))
.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.findByNo("1001", "DL-1")).thenReturn(delivery);
when(visits.findByDelivery("1001", 11L)).thenReturn(visit);
var result = service.updateDeliveryStatus("1001", "9001", "DL-1",
new CustomerVisitRequests.DeliveryStatus("DELIVERED", null));
assertThat(result.replayed()).isTrue();
assertThat(result.deliveryStatus()).isEqualTo("DELIVERED");
assertThat(result.visitId()).isEqualTo(33L);
verify(deliveryOrders, never()).updateById(delivery);
verifyNoInteractions(deliveryOperations);
}
private DeliveryOrder delivery(String status) {
DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(11L);
delivery.setCustomerId("1001");
delivery.setDeliveryNo("DL-1");
delivery.setStatus(status);
return delivery;
}
}
@@ -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, java.util.List.of());
}
}
@@ -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());
}
}
@@ -90,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());
@@ -167,6 +172,9 @@ class PlatformDispatchOutboundContractTest {
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"));
@@ -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", "MARK_RETURN");
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();
}
}
@@ -34,6 +34,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;
@@ -78,6 +80,7 @@ class DeliveryTransferServiceTest {
@Mock InventoryBalanceMapper inventoryBalances;
@Mock DropshipOrderProfileMapper dropshipProfiles;
@Mock DropshipCartonMapper dropshipCartons;
@Mock DropshipSkuInventoryMapper dropshipSkuInventory;
private DeliveryTransferService service;
@@ -103,6 +106,7 @@ class DeliveryTransferServiceTest {
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
ReflectionTestUtils.setField(service, "dropshipCartons", dropshipCartons);
ReflectionTestUtils.setField(service, "dropshipSkuInventory", dropshipSkuInventory);
}
@Test
@@ -398,12 +402,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(
@@ -411,9 +411,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,
@@ -652,4 +686,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;
}
}