fix(lastleg): repair dispatch cancellation and return execution contracts
This commit is contained in:
+20
@@ -24,8 +24,22 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomerVisitServiceTest {
|
||||
/** 未出库不得开始或拒签,已发车也没有人工取消旁路。 */
|
||||
@Test void manualExecutionRequiresActualOutboundAndNeverAcceptsCancellation() {
|
||||
var delivery = delivery("WAIT_DELIVERY");
|
||||
when(deliveryOrders.lockByNo("1001", "DL-1")).thenReturn(delivery);
|
||||
when(outboundOrders.lockByNo("1001", "OUT-1")).thenReturn(null);
|
||||
for (String target : java.util.List.of("DELIVERING", "REJECTED"))
|
||||
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus(target, 0L, null))).hasMessageContaining("尚未实际出库");
|
||||
assertThatThrownBy(() -> service.updateDeliveryStatus("1001", "9001", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus("CANCELLED", 0L, null))).hasMessageContaining("不支持");
|
||||
verify(deliveryOrders, never()).updateById(delivery);
|
||||
verifyNoInteractions(deliveryOperations);
|
||||
}
|
||||
@Mock private CustomerVisitMapper visits;
|
||||
@Mock private DeliveryOrderMapper deliveryOrders;
|
||||
@Mock private com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper outboundOrders;
|
||||
@Mock private DeliveryOperationMapper deliveryOperations;
|
||||
@Mock private LastlegOrderMapper lastMileOrders;
|
||||
private CustomerVisitService service;
|
||||
@@ -33,6 +47,7 @@ class CustomerVisitServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CustomerVisitService();
|
||||
ReflectionTestUtils.setField(service, "deliveryExecution", new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(outboundOrders, deliveryOrders));
|
||||
ReflectionTestUtils.setField(service, "visits", visits);
|
||||
ReflectionTestUtils.setField(service, "deliveryOrders", deliveryOrders);
|
||||
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
|
||||
@@ -97,6 +112,11 @@ class CustomerVisitServiceTest {
|
||||
delivery.setCustomerId("1001");
|
||||
delivery.setDeliveryNo("DL-1");
|
||||
delivery.setStatus(status);
|
||||
delivery.setOutboundOrderId(22L); delivery.setOutboundNo("OUT-1");
|
||||
var outbound = new com.baiyue.lastleg.delivery.entity.OutboundOrder();
|
||||
outbound.setId(22L); outbound.setCustomerId("1001"); outbound.setOutboundNo("OUT-1"); outbound.setStatus("OUTBOUNDED");
|
||||
org.mockito.Mockito.lenient().when(deliveryOrders.findByNo("1001", "DL-1")).thenReturn(delivery);
|
||||
org.mockito.Mockito.lenient().when(outboundOrders.lockByNo("1001", "OUT-1")).thenReturn(outbound);
|
||||
return delivery;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -24,6 +24,15 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DeliveryFeeCoverageServiceTest {
|
||||
/** 同一排车明细中的外接货覆盖不能免掉普通来源的应收。 */
|
||||
@Test void externalFreightCoversOnlyItsOwnInboundEvenWhenAnItemIsShared() {
|
||||
when(finance.sources(anyString(), any())).thenReturn(ApiResponse.success(List.of(fee(101, 11))));
|
||||
var preview = service.preview("DP-1");
|
||||
var selection = new DeliveryFeeCoverageModels.Selection(711L, 1, fee(101, 11).sourceHash(), List.of("LOADING_FEE"));
|
||||
var result = service.confirm("DP-1", coverageRequest(preview.cargoHash(), List.of(selection)), "OP", Set.of("IN-A"));
|
||||
assertThat(result.includedServices()).doesNotContain("DELIVERY_FEE");
|
||||
assertThat(result.uncovered("DELIVERY_FEE")).extracting(DeliveryFeeCoverageModels.Cargo::inboundCode).containsExactly("IN-B");
|
||||
}
|
||||
final DispatchOrderMapper dispatches = mock(DispatchOrderMapper.class);
|
||||
final DispatchItemMapper items = mock(DispatchItemMapper.class);
|
||||
final DispatchFeeCargoMapper cargo = mock(DispatchFeeCargoMapper.class);
|
||||
|
||||
+30
-2
@@ -12,6 +12,34 @@ import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DispatchFeeCargoMapperTest {
|
||||
/** 原退回实收只重派其已冻结货量,不借同托其他货物放大来源。 */
|
||||
@Test void returnedReceiptRetainsItsExactSourceInsteadOfBecomingAWholePallet() {
|
||||
var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101");
|
||||
var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L);
|
||||
item.setReturnReceiptId(701L); item.setSourceStage("RECEIVED"); item.setInboundCode("IN-A");
|
||||
item.setPackageCount(2); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8");
|
||||
var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper));
|
||||
assertThat(query.sources(dispatch, java.util.List.of(item))).singleElement().satisfies(source -> {
|
||||
assertThat(source.inboundCode()).isEqualTo("IN-A"); assertThat(source.packages()).isEqualTo(2);
|
||||
});
|
||||
}
|
||||
/** 展示普通货或外接货均不能改变真实组成;保留同客户、同仓及本次拣货边界。 */
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings={"IN-A", "IN-B"})
|
||||
void actualSourceQueryDoesNotDependOnWhichInboundWasChosenForDisplay(String displayInbound) {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET inbound_code=?,plan_pallet_id=801", displayInbound);
|
||||
var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101");
|
||||
var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L);
|
||||
item.setInboundCode(displayInbound); item.setPackageCount(20); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8");
|
||||
var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper));
|
||||
var sources = query.sources(dispatch, java.util.List.of(item));
|
||||
assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::inboundCode).containsExactly("IN-A", "IN-B");
|
||||
assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::packages).containsExactly(10, 10);
|
||||
item.setPackageCount(19);
|
||||
assertThatThrownBy(() -> query.sources(dispatch, java.util.List.of(item))).hasMessageContaining("箱数不一致");
|
||||
}
|
||||
JdbcTemplate jdbc; DispatchFeeCargoMapper mapper;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
@@ -41,10 +69,10 @@ class DispatchFeeCargoMapperTest {
|
||||
assertThat(rows).extracting(row -> row.packages()).containsExactly(10L, 10L);
|
||||
assertThat(mapper.list("202", 1)).isEmpty();
|
||||
}
|
||||
@Test void aSelectedPlanPalletCannotBorrowOtherInboundStockOnTheSamePhysicalPallet() {
|
||||
@Test void aPhysicalSharedPalletIncludesEveryActualSourceRegardlessOfItsDisplayPlan() {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET plan_pallet_id=801");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(801,901)");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_line VALUES(901,'101',401)");
|
||||
assertThat(mapper.list("101", 1)).extracting(row -> row.sourceInboundId()).containsExactly("11");
|
||||
assertThat(mapper.list("101", 1)).extracting(row -> row.sourceInboundId()).containsExactly("11", "12");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ class DeliveryStatusPolicyTest {
|
||||
@Test
|
||||
void exposesOnlyServerAuthorizedActionsForEachState() {
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("WAIT_DELIVERY"))
|
||||
.containsExactly("START_DELIVERY", "REJECT_DELIVERY", "CANCEL_DELIVERY");
|
||||
.containsExactly("START_DELIVERY", "REJECT_DELIVERY");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERING"))
|
||||
.containsExactly("REJECT_DELIVERY", "MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERED")).containsExactly("MARK_RETURN");
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DeliveryExecutionGuardTest {
|
||||
/** 直接消费真实仓储访问适配器,状态和数量由生产门禁判定。 */
|
||||
@Test void departureRequiresCompleteExecutableDeliveriesAndLocksThem() {
|
||||
var deliveries = mock(DeliveryOrderMapper.class);
|
||||
var guard = new DeliveryExecutionGuard(new DeliveryExecutionRepository(mock(OutboundOrderMapper.class), deliveries));
|
||||
var outbound = new OutboundOrder(); outbound.setId(1L); outbound.setCustomerId("C");
|
||||
outbound.setOutboundNo("OUT-1"); outbound.setPackageCount(2);
|
||||
var delivery = new DeliveryOrder(); delivery.setCustomerId("C"); delivery.setOutboundOrderId(1L);
|
||||
delivery.setOutboundNo("OUT-1"); delivery.setStatus("WAIT_DELIVERY"); delivery.setPackageCount(2);
|
||||
when(deliveries.lockByOutbound("C", 1L)).thenReturn(List.of(delivery));
|
||||
guard.requireDeparture(outbound);
|
||||
delivery.setStatus("CANCELLED");
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("已取消");
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setPackageCount(1);
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("完整覆盖");
|
||||
delivery.setPackageCount(2); delivery.setCustomerId("OTHER");
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("来源不一致");
|
||||
when(deliveries.lockByOutbound("C", 1L)).thenReturn(List.of());
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("缺少对应派送单");
|
||||
verify(deliveries, times(5)).lockByOutbound("C", 1L);
|
||||
}
|
||||
}
|
||||
+1
@@ -26,6 +26,7 @@ class DeliveryReturnEvidencePersistenceTest {
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick_order(customer_id VARCHAR(20),pick_id BIGINT,order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_ledger(id BIGINT,customer_id VARCHAR(20),business_order_no VARCHAR(20),outbound_order_no VARCHAR(20),business_type VARCHAR(40),delta_packages BIGINT,source_event_id VARCHAR(100),occurred_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_return_carton_stock_receipt(id BIGINT,customer_id VARCHAR(20),source_dispatch_item_id BIGINT,source_outbound_pallet_item_id BIGINT,packages BIGINT,received_at TIMESTAMP,delivery_order_id BIGINT,receipt_no VARCHAR(64))");
|
||||
jdbc.update("INSERT INTO ll_delivery_order VALUES(1,'A',10,20,'OUT-1')");
|
||||
jdbc.update("INSERT INTO ll_exception_order VALUES(2,'A',1,'EX-1',71)");
|
||||
jdbc.update("INSERT INTO ll_exception_return_item VALUES(2,'A',21,41)");
|
||||
|
||||
+10
@@ -134,6 +134,16 @@ class DeliveryTransferServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DeliveryTransferService();
|
||||
ReflectionTestUtils.setField(service, "deliveryExecution", new DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(outboundOrders, deliveryOrders)));
|
||||
var actualSources = mock(DispatchCargoSourceQuery.class);
|
||||
ReflectionTestUtils.setField(service, "cargoSources", actualSources);
|
||||
org.mockito.Mockito.lenient().when(actualSources.sources(any(), any())).thenAnswer(call -> {
|
||||
List<DispatchItem> selected = call.getArgument(1);
|
||||
return selected.stream().map(item -> new com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo(
|
||||
item.getInboundCode(), item.getWarehousePalletId() == null ? null : item.getWarehousePalletId().toString(), item.getPalletNo(),
|
||||
java.util.Objects.requireNonNullElse(item.getPackageCount(), 0), com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(
|
||||
item.getDestinationType(), item.getDestinationWarehouseCode(), item.getFbaWarehouseCode(), item.getDeliveryAddress(), item.getPostalCode()))).toList();
|
||||
});
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
ReflectionTestUtils.setField(service, "bookingFreight", bookingFreight);
|
||||
ReflectionTestUtils.setField(service, "feeCoverage", feeCoverage);
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ class DispatchPdaWorkflowServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "deliveryExecution", org.mockito.Mockito.mock(com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "fleetPool", org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge.class));
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.dispatch.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
|
||||
import com.baiyue.lastleg.fleet.trip.*;
|
||||
import com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class DispatchAppointmentCancellationPersistenceTest {
|
||||
/** 实际取消只解除有效预约运输占用,历史引用和后来重排的占用都保留。 */
|
||||
@Test void cancellationReleasesEffectiveBatchAllocationAndReplayCannotReleaseTheNewDispatch() throws Exception {
|
||||
var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
var db=new JdbcTemplate(data);var tx=new TransactionTemplate(new DataSourceTransactionManager(data));
|
||||
var config=new MybatisConfiguration();var mappers=List.of(DispatchOrderMapper.class,DispatchItemMapper.class,DispatchPalletLockMapper.class,OutboundOrderMapper.class,FleetTripDispatchMapper.class,AppointmentWorkDispatchMapper.class);
|
||||
mappers.forEach(config::addMapper);var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet();var session=new SqlSessionTemplate(factory.getObject());
|
||||
for(var mapper:mappers)for(var parent:mapper.getGenericInterfaces()) {
|
||||
if(!(parent instanceof java.lang.reflect.ParameterizedType p)||p.getRawType()!=com.baomidou.mybatisplus.core.mapper.BaseMapper.class)continue;
|
||||
var table=TableInfoHelper.getTableInfo((Class<?>)p.getActualTypeArguments()[0]);var columns=new ArrayList<String>();columns.add(table.getKeyColumn()+" BIGINT PRIMARY KEY");
|
||||
table.getFieldList().forEach(f->columns.add(f.getColumn()+" "+sqlType(f.getPropertyType())));db.execute("CREATE TABLE "+table.getTableName()+"("+String.join(",",columns)+")");
|
||||
}
|
||||
db.execute("CREATE TABLE ll_dispatch_appointment_allocation(dispatch_id BIGINT,dispatch_item_id BIGINT,plan_pallet_id BIGINT,batch_key VARCHAR(64),customer_id VARCHAR(64),source_key VARCHAR(64),destination_key VARCHAR(64),package_count INT)");
|
||||
db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(10,'C','DP-OLD','PENDING_DEPARTURE')");
|
||||
db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,plan_pallet_id,package_count) VALUES(30,'C',10,1,4)");
|
||||
db.update("INSERT INTO ll_outbound_order(id,customer_id,dispatch_order_id,status) VALUES(40,'C',10,'CANCELLED')");
|
||||
db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,status,active_key) VALUES(50,'C',10,'ACTIVE','RECEIVED:501')");
|
||||
db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(10,30,1,'FBA_APPOINTMENT:1','C','ORDER:1','FBA:ONT8',4)");
|
||||
var allocations=session.getMapper(AppointmentWorkDispatchMapper.class);
|
||||
assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isEqualTo(4);
|
||||
var service=new DispatchService();ReflectionTestUtils.setField(service,"orders",session.getMapper(DispatchOrderMapper.class));ReflectionTestUtils.setField(service,"items",session.getMapper(DispatchItemMapper.class));ReflectionTestUtils.setField(service,"locks",session.getMapper(DispatchPalletLockMapper.class));ReflectionTestUtils.setField(service,"outboundOrders",session.getMapper(OutboundOrderMapper.class));ReflectionTestUtils.setField(service,"fleetBindings",new FleetDispatchBindingGuard(session.getMapper(FleetTripDispatchMapper.class),new ObjectMapper()));ReflectionTestUtils.setField(service,"workChanges",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class));
|
||||
tx.executeWithoutResult(s->service.cancel("DP-OLD"));
|
||||
assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isZero();
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_appointment_allocation WHERE dispatch_id=10",Long.class)).isEqualTo(1);
|
||||
db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(11,'C','DP-NEW','PENDING_DEPARTURE')");
|
||||
db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,status,active_key) VALUES(51,'C',11,'ACTIVE','RECEIVED:501')");
|
||||
db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(11,31,1,'FBA_APPOINTMENT:1','C','ORDER:1','FBA:ONT8',4)");
|
||||
tx.executeWithoutResult(s->service.cancel("DP-OLD"));
|
||||
assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isEqualTo(4);
|
||||
assertThat(db.queryForObject("SELECT status FROM ll_dispatch_pallet_lock WHERE id=51",String.class)).isEqualTo("ACTIVE");
|
||||
assertThat(db.queryForObject("SELECT active_key FROM ll_dispatch_pallet_lock WHERE id=50",String.class)).isNull();
|
||||
}
|
||||
/** 测试表保留生产Mapper列映射,正式DDL由独立Schema合同验证。 */
|
||||
private static String sqlType(Class<?> type){return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(4000)";}
|
||||
}
|
||||
+2
-2
@@ -55,7 +55,7 @@ class DispatchBolPrintServiceTest {
|
||||
null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE",
|
||||
1, 1, 1, 0, 0, 1, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(),
|
||||
List.of(new DispatchResponse.BolLine("TSTU3539519", "DP-260911-0027", "ABE2", "ABE2",
|
||||
"NYEXX2609110214", null, null, 1)), null, List.of());
|
||||
"NYEXX2609110214", null, null, 1)), null, List.of(), null);
|
||||
var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga",
|
||||
null, "91730", "10767 7th Street", null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
@@ -87,7 +87,7 @@ class DispatchBolPrintServiceTest {
|
||||
new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse",
|
||||
"NYEXX2609110214", null, null, 1),
|
||||
new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse",
|
||||
"NYEXX2609110215", null, null, 1)), null, List.of());
|
||||
"NYEXX2609110215", null, null, 1)), null, List.of(), null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE", null, null);
|
||||
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
|
||||
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.customervisit.dto.CustomerVisitRequests;
|
||||
import com.baiyue.lastleg.customervisit.mapper.CustomerVisitMapper;
|
||||
import com.baiyue.lastleg.customervisit.service.CustomerVisitService;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard;
|
||||
import com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery;
|
||||
import com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository;
|
||||
import com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter;
|
||||
import com.baiyue.lastleg.dispatch.mapper.*;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.*;
|
||||
import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionReturnItemMapper;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
||||
import com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService;
|
||||
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
|
||||
import com.baiyue.lastleg.fleet.trip.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelBindingGuard;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.ibatis.session.LocalCacheScope;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 连续调用真实服务、Mapper及事务;仅隔离外部通知、财务和MySQL专用候选聚合。 */
|
||||
class DispatchRepairWorkflowTest {
|
||||
private JdbcTemplate db;
|
||||
private TransactionTemplate transactions;
|
||||
private SqlSessionTemplate session;
|
||||
private final Map<Class<?>, Object> dependencies = new LinkedHashMap<>();
|
||||
private final ObjectMapper json = new ObjectMapper().findAndRegisterModules();
|
||||
private DispatchService dispatch;
|
||||
private DeliveryTransferService delivery;
|
||||
private CustomerVisitService visits;
|
||||
private ExceptionOrderService exceptions;
|
||||
private DispatchFleetAdapter fleet;
|
||||
private DispatchCandidateMapper candidates;
|
||||
private OverseasFeeSourceService fees;
|
||||
|
||||
/** 每个用例使用独立H2库,不连接运行环境;真实Mapper元数据生成最小测试表。 */
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID()
|
||||
+ ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
db = new JdbcTemplate(source);
|
||||
transactions = new TransactionTemplate(new DataSourceTransactionManager(source));
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
|
||||
List<Class<?>> mappers = List.of(DispatchOrderMapper.class, DispatchItemMapper.class,
|
||||
DispatchPalletLockMapper.class, DispatchBolMapper.class, DispatchPdaTaskMapper.class,
|
||||
DispatchPdaPickMapper.class, DispatchPdaPickOrderMapper.class, OutboundOrderMapper.class,
|
||||
OutboundPalletItemMapper.class, OutboundPdaTaskMapper.class, OutboundPdaPickMapper.class,
|
||||
DeliveryOrderMapper.class, DeliveryOperationMapper.class, CustomerVisitMapper.class,
|
||||
LastlegOrderMapper.class, LastlegInboundOrderMapper.class, LastlegOrderStatusHistoryMapper.class,
|
||||
FleetTripDispatchMapper.class, FleetTripMapper.class, FleetBookingMapper.class, InventoryPositionMapper.class,
|
||||
InventoryBalanceMapper.class, DispatchFeeCargoMapper.class, ExceptionReturnItemMapper.class, ExceptionOrderMapper.class,
|
||||
RelabelApplicationMapper.class, RelabelItemMapper.class, PdaRelabelHistoryMapper.class,
|
||||
RelabelPalletMeasurementMapper.class, ReturnInventoryCartonMapper.class, DispatchCandidateMapper.class,
|
||||
WarehouseTransferOrderMapper.class);
|
||||
mappers.forEach(configuration::addMapper);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(source);
|
||||
factory.setConfiguration(configuration);
|
||||
factory.afterPropertiesSet();
|
||||
session = new SqlSessionTemplate(factory.getObject());
|
||||
for (Class<?> mapper : mappers) dependencies.put(mapper, session.getMapper(mapper));
|
||||
// 不复制正式SQL,也不声称验证生产DDL;只构造本文件使用的实体表。
|
||||
for (Class<?> mapper : mappers) for (var parent : mapper.getGenericInterfaces()) {
|
||||
if (!(parent instanceof java.lang.reflect.ParameterizedType generic)
|
||||
|| generic.getRawType() != com.baomidou.mybatisplus.core.mapper.BaseMapper.class) continue;
|
||||
var table = TableInfoHelper.getTableInfo((Class<?>) generic.getActualTypeArguments()[0]);
|
||||
var columns = new ArrayList<String>();
|
||||
if (table.havePK()) columns.add(table.getKeyColumn() + " " + sqlType(table.getKeyType())
|
||||
+ (table.getIdType() == com.baomidou.mybatisplus.annotation.IdType.AUTO ? " AUTO_INCREMENT" : "") + " PRIMARY KEY");
|
||||
table.getFieldList().forEach(field -> columns.add(field.getColumn() + " " + sqlType(field.getPropertyType())));
|
||||
db.execute("CREATE TABLE IF NOT EXISTS " + table.getTableName() + " (" + String.join(",", columns) + ")");
|
||||
}
|
||||
db.execute("CREATE UNIQUE INDEX test_dispatch_active_lock ON ll_dispatch_pallet_lock(customer_id,active_key)");
|
||||
// 普通换标与退货换标候选查询含MySQL专用聚合;固定输入不替代完整集合校验或状态写入。
|
||||
candidates = spy(session.getMapper(DispatchCandidateMapper.class));
|
||||
doReturn(List.of()).when(candidates).nativeReturnRelabelAvailable(any(), any(), any());
|
||||
dependencies.put(DispatchCandidateMapper.class, candidates);
|
||||
dependencies.put(ObjectMapper.class, json);
|
||||
dependencies.put(LastlegOrderNoGenerator.class, new LastlegOrderNoGenerator());
|
||||
dependencies.put(FleetDispatchBindingGuard.class,
|
||||
new FleetDispatchBindingGuard(mapper(FleetTripDispatchMapper.class), json));
|
||||
dependencies.put(FleetDispatchWorkflowQuery.class,
|
||||
new FleetDispatchWorkflowQuery(mapper(FleetTripDispatchMapper.class), mapper(FleetTripMapper.class), json));
|
||||
var execution = new DeliveryExecutionRepository(mapper(OutboundOrderMapper.class), mapper(DeliveryOrderMapper.class));
|
||||
dependencies.put(DeliveryExecutionRepository.class, execution);
|
||||
dependencies.put(DeliveryExecutionGuard.class, new DeliveryExecutionGuard(execution));
|
||||
dependencies.put(DispatchCargoSourceQuery.class,
|
||||
new DispatchCargoSourceQuery(new DispatchCargoSourceRepository(mapper(DispatchFeeCargoMapper.class))));
|
||||
dependencies.put(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class,
|
||||
mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class));
|
||||
dependencies.put(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class,
|
||||
mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class));
|
||||
fees = mock(OverseasFeeSourceService.class);
|
||||
dependencies.put(OverseasFeeSourceService.class, fees);
|
||||
var binding = wire(new RelabelBindingGuard());
|
||||
var measurements = wire(new RelabelPalletMeasurementService());
|
||||
var returnStock = wire(new ReturnRelabelDispatchStock());
|
||||
dependencies.put(RelabelBindingGuard.class, binding);
|
||||
dependencies.put(RelabelPalletMeasurementService.class, measurements);
|
||||
dependencies.put(ReturnRelabelDispatchStock.class, returnStock);
|
||||
dispatch = wire(new DispatchService());
|
||||
delivery = wire(new DeliveryTransferService());
|
||||
visits = wire(new CustomerVisitService());
|
||||
exceptions = wire(new ExceptionOrderService());
|
||||
fleet = new DispatchFleetAdapter(mapper(DispatchOrderMapper.class), mapper(DispatchPdaTaskMapper.class),
|
||||
mapper(OutboundOrderMapper.class), mapper(DeliveryOrderMapper.class), mapper(DispatchItemMapper.class),
|
||||
mapper(DispatchPdaPickMapper.class), new DispatchPdaWorkflowService(), mapper(DeliveryOperationMapper.class));
|
||||
wire(fleet);
|
||||
}
|
||||
|
||||
/** 历史取消出库仍可追溯;取消旧排车后同货重排,重复取消不得释放新锁。 */
|
||||
@Test
|
||||
void cancelledOutboundThenDispatchCancellationAllowsNewArrangementWithoutReleasingItsLock() {
|
||||
seedPendingOutbound();
|
||||
db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,plan_pallet_id,warehouse_pallet_id,source_pallet_key,active_key,status) VALUES(40,'C',1,900,501,'PLAN:900','PLAN:900','ACTIVE')");
|
||||
run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("重新安排")));
|
||||
assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("CANCELLED");
|
||||
assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("CANCELLED");
|
||||
long operations = count("ll_delivery_operation");
|
||||
run(() -> dispatch.cancel("DP-1"));
|
||||
assertThat(text("ll_dispatch_order", "status", 1)).isEqualTo("CANCELLED");
|
||||
assertThat(text("ll_dispatch_pallet_lock", "status", 40)).isEqualTo("RELEASED");
|
||||
assertThat(db.queryForObject("SELECT active_key FROM ll_dispatch_pallet_lock WHERE id=40", String.class)).isNull();
|
||||
doReturn(List.of(candidate(501).planPalletId(900L))).when(candidates).lockSelected("C", List.of(900L));
|
||||
var created = inTransaction(() -> dispatch.preArrange("C", "OP",
|
||||
new DispatchRequests.PreArrange(List.of(900L), null, null, null)));
|
||||
assertThat(created.dispatchNo()).isNotEqualTo("DP-1");
|
||||
assertThat(created.actions().canCancel()).isTrue();
|
||||
assertThat(created.actions().canTransfer()).isTrue();
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE' AND dispatch_order_id<>1", Long.class)).isEqualTo(1);
|
||||
run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("重复取消")));
|
||||
run(() -> dispatch.cancel("DP-1"));
|
||||
assertThat(count("ll_outbound_order")).isEqualTo(1);
|
||||
assertThat(count("ll_delivery_operation")).isEqualTo(operations);
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE' AND dispatch_order_id<>1", Long.class)).isEqualTo(1);
|
||||
verify(fees, times(1)).listForBusiness("C", "DELIVERY_ORDER", 3L);
|
||||
verify(fees, never()).voidSource(anyLong(), any(), any());
|
||||
}
|
||||
|
||||
/** 真实车次关联和已拣事实分别阻止取消,失败事务不修改单据及占用。 */
|
||||
@Test
|
||||
void bindingThenPickingIndependentlyPreventOutboundCancellation() {
|
||||
seedPendingOutbound();
|
||||
db.update("INSERT INTO ll_fleet_trip_dispatch(trip_id,trip_no,dispatch_id,dispatch_no,customer_id) VALUES(51,'TR-1',1,'DP-1','C')");
|
||||
assertThatThrownBy(() -> run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("取消"))))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("解除关联");
|
||||
assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND");
|
||||
db.update("DELETE FROM ll_fleet_trip_dispatch WHERE dispatch_id=1");
|
||||
db.update("UPDATE ll_dispatch_order SET picked_package_count=1 WHERE id=1");
|
||||
assertThatThrownBy(() -> run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("取消"))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND");
|
||||
assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY");
|
||||
assertThat(count("ll_delivery_operation")).isZero();
|
||||
}
|
||||
|
||||
/** 未出库开始派送/拒签均拒绝;真实出库后允许开始,但普通状态编辑始终不能取消。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"DELIVERING", "REJECTED"})
|
||||
void deliveryActionsFollowActualOutboundAndNeverOfferAnIndependentCancellation(String target) {
|
||||
seedPendingOutbound();
|
||||
assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus(target, 0L, "提前操作"))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY");
|
||||
assertThat(count("ll_delivery_operation")).isZero();
|
||||
db.update("UPDATE ll_dispatch_order SET status='DEPARTED',shipped_package_count=2 WHERE id=1");
|
||||
db.update("UPDATE ll_outbound_order SET status='OUTBOUNDED' WHERE id=2");
|
||||
assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus("CANCELLED", 0L, "已发车取消"))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY");
|
||||
run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus(target, 0L, "合法操作")));
|
||||
assertThat(text("ll_delivery_order", "status", 3)).isEqualTo(target);
|
||||
assertThat(db.queryForObject("SELECT version FROM ll_delivery_order WHERE id=3", Long.class)).isEqualTo(1);
|
||||
long operations = count("ll_delivery_operation");
|
||||
var replay = inTransaction(() -> visits.updateDeliveryStatus("C", "OP", "DL-1",
|
||||
new CustomerVisitRequests.DeliveryStatus(target, 0L, "同状态重放")));
|
||||
assertThat(replay.replayed()).isTrue();
|
||||
assertThat(count("ll_delivery_operation")).isEqualTo(operations);
|
||||
}
|
||||
|
||||
/** 已完成拣货仍须完整覆盖未执行派送;取消、缺失或数量失配均阻断发车。 */
|
||||
@Test
|
||||
void departureManifestRejectsCancelledMissingAndIncompleteDeliveryCoverage() {
|
||||
seedPendingOutbound();
|
||||
db.update("UPDATE ll_dispatch_order SET status='READY_TO_DEPART',picked_package_count=2 WHERE id=1");
|
||||
db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id,status,warehouse_id) VALUES(102,'C',1,'COMPLETED','WH-1')");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,inventory_position_id,packages) VALUES(103,'C',102,4,104,2)");
|
||||
assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNull();
|
||||
db.update("UPDATE ll_delivery_order SET status='CANCELLED' WHERE id=3");
|
||||
assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank();
|
||||
var guard = (DeliveryExecutionGuard) dependencies.get(DeliveryExecutionGuard.class);
|
||||
assertThatThrownBy(() -> run(() -> guard.requireDeparture(mapper(OutboundOrderMapper.class).findByNo("C", "OUT-1"))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
db.update("UPDATE ll_delivery_order SET status='WAIT_DELIVERY',package_count=1 WHERE id=3");
|
||||
assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank();
|
||||
db.update("DELETE FROM ll_delivery_order WHERE id=3");
|
||||
assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank();
|
||||
assertThat(text("ll_dispatch_order", "status", 1)).isEqualTo("READY_TO_DEPART");
|
||||
assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND");
|
||||
}
|
||||
|
||||
/** 两托部分选取和重复ID都必须零副作用,完整预排取消后仍可整单重新预排。 */
|
||||
@Test
|
||||
void ordinaryRelabelRequiresAllPalletsAndCanBeRearrangedAfterCancellation() {
|
||||
db.update("INSERT INTO ll_relabel_application(id,customer_id,relabel_no,overseas_warehouse_id,status,fulfillment_status,delivery_method) VALUES(70,'C','RL-1','WH-1','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH','TRUCK')");
|
||||
for (long pallet : List.of(501L, 502L)) {
|
||||
db.update("INSERT INTO ll_inventory_position(id,customer_id,pallet_id,pallet_no,overseas_warehouse_id,hold_packages) VALUES(?,'C',?,?,'WH-1',1)", pallet, pallet, "P-" + pallet);
|
||||
db.update("INSERT INTO ll_relabel_item(id,customer_id,application_id,operation_type,source_inventory_position_id,carton_mark,notified_package_count) VALUES(?,'C',70,'CARTON_LABEL',?,?,1)", pallet, pallet, "BOX-" + pallet);
|
||||
}
|
||||
db.update("INSERT INTO ll_relabel_pda_history(id,customer_id,application_id,action,detail_json) VALUES(71,'C',70,'PALLET_BIND',?)",
|
||||
"{\"cartonMarks\":[\"BOX-501\",\"BOX-502\"],\"targetPallets\":[\"P-501\",\"P-502\"]}");
|
||||
doReturn(List.of(candidate(501).orderRefs("RL-1").packageCount(1), candidate(502).orderRefs("RL-1").packageCount(1)))
|
||||
.when(candidates).relabelAvailable("C", null, "RL-1");
|
||||
for (var selection : List.of(List.of(501L), List.of(501L, 501L, 502L))) {
|
||||
assertThatThrownBy(() -> inTransaction(() -> dispatch.preArrange("C", "OP", relabel(selection))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
assertThat(count("ll_dispatch_order")).isZero();
|
||||
assertThat(count("ll_dispatch_item")).isZero();
|
||||
assertThat(count("ll_dispatch_pda_task")).isZero();
|
||||
assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("WAIT_PRE_DISPATCH");
|
||||
}
|
||||
var first = inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L))));
|
||||
assertThat(first.actions().canRemovePallet()).isFalse();
|
||||
assertThat(count("ll_dispatch_item")).isEqualTo(2);
|
||||
assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("PRE_DISPATCH_CREATED");
|
||||
assertThatThrownBy(() -> inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L)))))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
run(() -> dispatch.cancel(first.dispatchNo()));
|
||||
assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("WAIT_PRE_DISPATCH");
|
||||
var second = inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L))));
|
||||
assertThat(second.dispatchNo()).isNotEqualTo(first.dispatchNo());
|
||||
assertThat(count("ll_dispatch_order")).isEqualTo(2);
|
||||
}
|
||||
|
||||
/** 同实托展示任一来源,都要按真实组成绑定完整外接货并保留普通货收费资格。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"IN-NORMAL", "IN-EXT"})
|
||||
void mixedPalletUsesBothActualSourcesForBindingAndFreight(String displayInbound) throws Exception {
|
||||
seedPendingOutbound();
|
||||
db.update("UPDATE ll_dispatch_item SET inbound_code=?,package_count=5 WHERE id=4", displayInbound);
|
||||
db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(80,'C',81,'IN-NORMAL'),(82,'C',83,'IN-EXT')");
|
||||
db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages) VALUES(84,'C',80,501,'P-501','WH-1',3),(85,'C',82,501,'P-501','WH-1',2)");
|
||||
db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code) VALUES(81,'C','LM-NORMAL','IN-NORMAL'),(83,'C','LM-EXT','IN-EXT')");
|
||||
// 该测试同时校验客户隔离,不能把同物理托盘编号的他客库存算进来。
|
||||
db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(86,'OTHER',87,'FOREIGN')");
|
||||
db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,overseas_warehouse_id,available_packages) VALUES(88,'OTHER',86,501,'WH-1',99)");
|
||||
db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)");
|
||||
db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan_line(id BIGINT,customer_id VARCHAR(64),order_id BIGINT)");
|
||||
var realCargo = mapper(DispatchFeeCargoMapper.class).list("C", 1);
|
||||
assertThat(realCargo).extracting(row -> row.inboundCode()).containsExactly("IN-NORMAL", "IN-EXT");
|
||||
assertThat(realCargo).extracting(row -> row.packages()).containsExactly(3L, 2L);
|
||||
var sources = inTransaction(() -> fleet.sourceCargo("DP-1"));
|
||||
assertThat(sources).extracting(FleetDispatchGateway.SourceCargo::inboundCode)
|
||||
.containsExactlyInAnyOrder("IN-NORMAL", "IN-EXT");
|
||||
assertThat(sources.stream().mapToInt(FleetDispatchGateway.SourceCargo::packages).sum()).isEqualTo(5);
|
||||
seedExternalBooking();
|
||||
var reservations = new FleetBookingReservations(mapper(FleetBookingMapper.class), json);
|
||||
var trip = new FleetTripEntity(); trip.setId(91L); trip.setTripNo("TR-1");
|
||||
run(() -> reservations.bindIncoming(trip,
|
||||
new FleetDispatchGateway.Snapshot(1L, "DP-1", "C", "WH-1", 5), sources, "OP"));
|
||||
assertThat(text("ll_fleet_external_booking", "status", 90)).isEqualTo("BOUND");
|
||||
db.update("INSERT INTO ll_fleet_trip_dispatch(trip_id,trip_no,dispatch_id,dispatch_no,customer_id) VALUES(91,'TR-1',1,'DP-1','C')");
|
||||
var freight = new FleetBookingFreight(mapper(FleetBookingMapper.class), mapper(FleetTripDispatchMapper.class), reservations, json);
|
||||
var preview = inTransaction(() -> freight.preview(1L, "DP-1", "C", sources));
|
||||
assertThat(preview.requiresDeliveryFee()).isTrue();
|
||||
assertThat(preview.quotes()).singleElement().satisfies(quote -> {
|
||||
assertThat(quote.bookingNo()).isEqualTo("FB-EXT");
|
||||
assertThat(quote.incomingNo()).isEqualTo("WT-EXT");
|
||||
assertThat(quote.inboundCode()).isEqualTo("IN-EXT");
|
||||
assertThat(quote.amount()).isEqualByComparingTo("60.00");
|
||||
});
|
||||
assertThat(mapper(FleetBookingMapper.class).byNo("FB-EXT").getDispatchNo()).isEqualTo("DP-1");
|
||||
}
|
||||
|
||||
/** 退货换标真实箱来源必须成为可退候选,不得伪造普通库存仓位通过门禁。 */
|
||||
@Test
|
||||
void rejectedReturnRelabelRetainsItsNativeCartonSourceForExceptionReturn() {
|
||||
seedPendingOutbound();
|
||||
db.update("UPDATE ll_delivery_order SET status='REJECTED' WHERE id=3");
|
||||
db.update("UPDATE ll_outbound_order SET status='OUTBOUNDED' WHERE id=2");
|
||||
db.update("UPDATE ll_dispatch_item SET source_stage='RETURN_RELABEL',source_business_type='RELABEL_ORDER',source_business_no='RL-1',inbound_code='RL-1' WHERE id=4");
|
||||
db.update("UPDATE ll_outbound_pallet_item SET inbound_code='RL-1' WHERE id=5");
|
||||
db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code,delivery_order_no,outbound_order_no,package_count,overseas_warehouse_id) VALUES(100,'C','LM-RL','RL-1','DL-1','OUT-1',2,'WH-1')");
|
||||
db.update("INSERT INTO ll_inbound_order(id,customer_id,inbound_code,package_count) VALUES(104,'C','RL-1',2)");
|
||||
db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,carton_mark,pallet_code,overseas_warehouse_id,status,relabel_no,total_quantity) VALUES(101,'C','BOX-RL','P-501','WH-1','OUTBOUNDED','RL-1',6)");
|
||||
db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id,status,warehouse_id) VALUES(102,'C',1,'COMPLETED','WH-1')");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(103,'C',102,4,101,1,6)");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,packages,product_quantity) VALUES(105,'C',103,100,1,6)");
|
||||
var relations = inTransaction(() -> exceptions.returnCandidates("C", "DL-1"));
|
||||
assertThat(relations).hasSize(1);
|
||||
assertThat(relations.get(0).returnSources()).singleElement().satisfies(source -> {
|
||||
var actual = json.valueToTree(source);
|
||||
assertThat(actual.path("returnCartonId").asLong()).isEqualTo(101L);
|
||||
assertThat(source.maxReturnPackages()).isEqualTo(1);
|
||||
});
|
||||
assertThat(db.queryForObject("SELECT inventory_position_id FROM ll_dispatch_pda_pick WHERE id=103", Long.class)).isNull();
|
||||
assertThat(count("ll_inventory_position")).isZero();
|
||||
}
|
||||
|
||||
/** 准备统一未拣货排车、出库及派送关系,避免用桩伪造取消资格。 */
|
||||
private void seedPendingOutbound() {
|
||||
db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,overseas_warehouse_id,status,package_count,picked_package_count,shipped_package_count) VALUES(1,'C','DP-1','WH-1','PENDING_DEPARTURE',2,0,0)");
|
||||
db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,outbound_type,status,package_count) VALUES(2,'C','OUT-1',1,'DISPATCH_OUTBOUND','WAIT_OUTBOUND',2)");
|
||||
db.update("INSERT INTO ll_delivery_order(id,customer_id,delivery_no,outbound_no,outbound_order_id,dispatch_order_id,status,version,package_count) VALUES(3,'C','DL-1','OUT-1',2,1,'WAIT_DELIVERY',0,2)");
|
||||
db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,pallet_no,overseas_warehouse_id,package_count,destination_type,destination_warehouse_code) VALUES(4,'C',1,501,'P-501','WH-1',2,'FBA','ONT8')");
|
||||
db.update("INSERT INTO ll_outbound_pallet_item(id,customer_id,outbound_order_id,warehouse_pallet_id,pallet_no,package_count,picked_package_count,status,destination_warehouse_code) VALUES(5,'C',2,501,'P-501',2,0,'WAIT_PICK','ONT8')");
|
||||
}
|
||||
|
||||
/** 固定真实外接实收两箱及约定金额,后续绑定和报价核对均运行生产逻辑。 */
|
||||
private void seedExternalBooking() throws Exception {
|
||||
var cargo = new FleetBookingModels.Cargo("FBA:ONT8", "外接货", 2, 1, BigDecimal.TEN,
|
||||
BigDecimal.ONE, "PALLET", new BigDecimal("60.00"), "CNY", null, null, null);
|
||||
var receipt = new FleetBookingModels.AcceptedReceipt(new FleetBookingIncomingGateway.Evidence(
|
||||
"WT-EXT", "IN-EXT", "WH-1", "FBA:ONT8", "READY_FOR_DISPATCH", "receipt-v1",
|
||||
List.of(new FleetBookingIncomingGateway.Pallet("501", "P-501", 2, BigDecimal.TEN, BigDecimal.ONE)), 2),
|
||||
List.of(new FleetBookingModels.PalletSpaces("501", 1, null, null)), "OP", Instant.parse("2026-10-01T00:00:00Z"));
|
||||
// 来货业务编号和库存入仓码是两个标识,必须经真实来货登记关联,不能混用。
|
||||
db.update("INSERT INTO ll_warehouse_transfer_order(id,customer_id,transfer_no,inbound_code,status) VALUES(93,'C','WT-EXT','IN-EXT','READY_FOR_DISPATCH')");
|
||||
db.update("INSERT INTO ll_fleet_external_booking(id,trip_id,trip_no,booking_no,incoming_no,customer_id,destination_key,status,agreed_amount,record_version,cargo_json,receipt_json) VALUES(90,91,'TR-1','FB-EXT','WT-EXT','C','FBA:ONT8','RECEIVED',60,1,?,?)",
|
||||
json.writeValueAsString(cargo), json.writeValueAsString(receipt));
|
||||
}
|
||||
|
||||
/** 构造来源输入;真实门禁仍由DispatchService及数据库锁决定。 */
|
||||
private DispatchPalletCandidate candidate(long pallet) {
|
||||
return new DispatchPalletCandidate().warehousePalletId(pallet).palletNo("P-" + pallet)
|
||||
.customerId("C").overseasWarehouseId("WH-1").sourceStage("RECEIVED")
|
||||
.destinationType("FBA").destinationWarehouseCode("ONT8").fbaWarehouseCode("ONT8")
|
||||
.packageCount(2).grossWeightKg(BigDecimal.TEN).volumeCbm(BigDecimal.ONE);
|
||||
}
|
||||
|
||||
/** 保留整单换标申请入口,不直接调用私有校验。 */
|
||||
private DispatchRequests.PreArrange relabel(List<Long> pallets) {
|
||||
return new DispatchRequests.PreArrange(pallets, null, null, null, null, "RL-1");
|
||||
}
|
||||
|
||||
/** 按类型注入实际依赖;未用到的外部能力保持未装配,调用时显式失败。 */
|
||||
private <T> T wire(T target) {
|
||||
for (var field : target.getClass().getDeclaredFields()) {
|
||||
if (!field.isAnnotationPresent(Resource.class)) continue;
|
||||
Object value = dependencies.get(field.getType());
|
||||
if (value != null) ReflectionTestUtils.setField(target, field.getName(), value);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/** 从当前隔离库取得生产Mapper代理。 */
|
||||
private <T> T mapper(Class<T> type) {
|
||||
return type.cast(dependencies.get(type));
|
||||
}
|
||||
|
||||
/** 每条业务命令独立提交;失败必须整条回滚,不能依赖测试整体回滚隐藏副作用。 */
|
||||
private <T> T inTransaction(Supplier<T> action) {
|
||||
return transactions.execute(status -> action.get());
|
||||
}
|
||||
|
||||
/** 对无返回值命令应用同样的真实事务边界。 */
|
||||
private void run(Runnable action) {
|
||||
inTransaction(() -> { action.run(); return null; });
|
||||
}
|
||||
|
||||
/** 仅查询本文件常量指定的测试表,不接收用户输入。 */
|
||||
private long count(String table) {
|
||||
return db.queryForObject("SELECT COUNT(*) FROM " + table, Long.class);
|
||||
}
|
||||
|
||||
/** 从数据库重新读取状态,避免断言内存实体误当持久化成功。 */
|
||||
private String text(String table, String column, long id) {
|
||||
return db.queryForObject("SELECT " + column + " FROM " + table + " WHERE id=?", String.class, id);
|
||||
}
|
||||
|
||||
/** 测试表仅表达实体字段类型,DDL、索引及MySQL行为另由正式合同测试校验。 */
|
||||
private String sqlType(Class<?> type) {
|
||||
if (Number.class.isAssignableFrom(type) || type == long.class || type == int.class)
|
||||
return type == BigDecimal.class ? "DECIMAL(24,6)" : "BIGINT";
|
||||
if (type == LocalDateTime.class || type == Instant.class) return "TIMESTAMP";
|
||||
if (type == java.time.LocalDate.class) return "DATE";
|
||||
if (type == Boolean.class || type == boolean.class) return "BOOLEAN";
|
||||
return "VARCHAR(32768)";
|
||||
}
|
||||
}
|
||||
+36
@@ -51,6 +51,42 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchServiceTest {
|
||||
/** 历史取消出库不阻止取消原排车,重放不影响后续占用。 */
|
||||
@Test void cancelledOutboundAllowsOriginalDispatchCancellationAndPreservesAppointmentHistory() {
|
||||
var dispatch = new DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("100");
|
||||
dispatch.setDispatchNo("DP-OLD"); dispatch.setStatus("PENDING_DEPARTURE");
|
||||
var outbound = new com.baiyue.lastleg.delivery.entity.OutboundOrder(); outbound.setStatus("CANCELLED");
|
||||
var item = item(11L, "ONT8");
|
||||
when(orders.lockGlobalByDispatchNo("DP-OLD")).thenReturn(dispatch);
|
||||
when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(outbound);
|
||||
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item));
|
||||
var allocations = mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);
|
||||
ReflectionTestUtils.setField(service, "appointmentAllocations", allocations);
|
||||
service.cancel("DP-OLD"); service.cancel("DP-OLD");
|
||||
assertEquals("CANCELLED", dispatch.getStatus());
|
||||
verify(allocations, never()).removeItem(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong());
|
||||
verify(orders).updateById(dispatch);
|
||||
}
|
||||
|
||||
/** 普通换标部分选择与重复选择均在创建排车前拒绝。 */
|
||||
@Test void ordinaryRelabelRequiresTheWholeLockedApplicationBeforeAnyMutation() {
|
||||
var relabels = mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelApplications", relabels);
|
||||
ReflectionTestUtils.setField(service, "relabelBindingGuard", mock(com.baiyue.lastleg.relabel.service.RelabelBindingGuard.class));
|
||||
var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication();
|
||||
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND");
|
||||
application.setDeliveryMethod("TRUCK"); application.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
when(relabels.lockNo("100", "RL-1")).thenReturn(application);
|
||||
when(candidates.relabelAvailable("100", null, "RL-1")).thenReturn(List.of(candidate(10L, 900L, "ONT8"), candidate(11L, 901L, "ONT8")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP",
|
||||
new DispatchRequests.PreArrange(List.of(900L), null, null, null, null, "RL-1")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP",
|
||||
new DispatchRequests.PreArrange(List.of(900L, 900L), null, null, null, null, "RL-1")));
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
verify(pdaTasks, never()).insert(any(DispatchPdaTask.class));
|
||||
verify(candidates, never()).markRelabelPreDispatched(any(), any());
|
||||
}
|
||||
|
||||
@Test void detailReturnsEveryOriginalBatchSourceWithoutLegacyAppointmentNumber() {
|
||||
var dispatch=new DispatchOrder();dispatch.setId(1L);dispatch.setCustomerId("C");dispatch.setDispatchNo("DP-1");
|
||||
when(orders.findGlobalByDispatchNo("DP-1")).thenReturn(dispatch);
|
||||
|
||||
+2
-2
@@ -137,7 +137,7 @@ class ExceptionOrderLifecycleTest {
|
||||
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")));
|
||||
"PLT-201", 301L, "LOC-301", null)));
|
||||
|
||||
var result = service.complete("1001", "9001", "EX-1", request);
|
||||
|
||||
@@ -222,7 +222,7 @@ class ExceptionOrderLifecycleTest {
|
||||
|
||||
private ExceptionOrderRequests.Complete receipt(long amount) {
|
||||
return new ExceptionOrderRequests.Complete(1, "batch-1", "本次实际退回", List.of(101L),
|
||||
List.of(new ExceptionOrderRequests.ReturnReceipt(51L, 61L, amount, 201L, "PLT-201", 301L, "LOC-301")));
|
||||
List.of(new ExceptionOrderRequests.ReturnReceipt(51L, 61L, amount, 201L, "PLT-201", 301L, "LOC-301", null)));
|
||||
}
|
||||
|
||||
private String hashFor(Object request) {
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package com.baiyue.lastleg.exceptionorder.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.*;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.*;
|
||||
import com.baiyue.lastleg.exceptionorder.repository.ReturnCartonReturnRepository;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.dispatch.mapper.*;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.*;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.*;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ReturnCartonReturnPersistenceTest {
|
||||
JdbcTemplate db;TransactionTemplate tx;ReturnCartonReturnService service;
|
||||
ExceptionOrder order;ExceptionReturnItem item;SqlSessionTemplate session;
|
||||
/** 隔离H2中运行真实Mapper、来源归属查询、原生库存更新及事务。 */
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
db=new JdbcTemplate(data);tx=new TransactionTemplate(new DataSourceTransactionManager(data));
|
||||
var config=new MybatisConfiguration();
|
||||
var mappers=List.of(ExceptionOrderMapper.class,ExceptionOrderCommandMapper.class,com.baiyue.lastleg.mapper.LastlegOrderMapper.class,com.baiyue.lastleg.mapper.LastlegInboundOrderMapper.class,ExceptionReturnItemMapper.class,ReturnCartonStockReceiptMapper.class,
|
||||
ReturnInventoryCartonMapper.class,ReturnInventoryCartonItemMapper.class,ReturnInventoryBalanceMapper.class,ReturnInventoryMovementMapper.class,
|
||||
DeliveryOrderMapper.class,OutboundOrderMapper.class,OutboundPalletItemMapper.class,OutboundPdaPickMapper.class,OutboundPdaTaskMapper.class,DispatchOrderMapper.class,DispatchItemMapper.class,
|
||||
DispatchPdaTaskMapper.class,DispatchPdaPickMapper.class,DispatchPdaPickOrderMapper.class,DeliveryOperationMapper.class);
|
||||
mappers.forEach(config::addMapper);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet();
|
||||
session=new SqlSessionTemplate(factory.getObject());
|
||||
for(var mapper:mappers)for(var parent:mapper.getGenericInterfaces()) {
|
||||
if(!(parent instanceof java.lang.reflect.ParameterizedType p)||p.getRawType()!=com.baomidou.mybatisplus.core.mapper.BaseMapper.class)continue;
|
||||
var table=TableInfoHelper.getTableInfo((Class<?>)p.getActualTypeArguments()[0]);var columns=new ArrayList<String>();
|
||||
columns.add(table.getKeyColumn()+" BIGINT PRIMARY KEY");
|
||||
table.getFieldList().forEach(f->columns.add(f.getColumn()+" "+sqlType(f.getPropertyType())));
|
||||
db.execute("CREATE TABLE IF NOT EXISTS "+table.getTableName()+"("+String.join(",",columns)+")");
|
||||
}
|
||||
db.execute("CREATE UNIQUE INDEX balance_scope ON ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code)");
|
||||
db.execute("CREATE UNIQUE INDEX receipt_source ON ll_return_carton_stock_receipt(customer_id,source_pick_id,sku_code)");
|
||||
db.execute("CREATE TABLE ll_inventory_ledger(id BIGINT,customer_id VARCHAR(128),business_order_no VARCHAR(128),outbound_order_no VARCHAR(128),business_type VARCHAR(128),delta_packages BIGINT,source_event_id VARCHAR(128),occurred_at TIMESTAMP)");
|
||||
var repo=new ReturnCartonReturnRepository(map(ReturnCartonStockReceiptMapper.class),map(ReturnInventoryCartonMapper.class),map(ReturnInventoryCartonItemMapper.class),map(ReturnInventoryMovementMapper.class),map(ReturnInventoryBalanceMapper.class));
|
||||
var pallets=mock(PalletPhysicalGateway.class);var warehouse=mock(WarehouseResourceGateway.class);
|
||||
when(pallets.resolve("P-NEW")).thenReturn(new PalletPhysicalGateway.PalletReference(501,"P-NEW",92,301L,"OCCUPIED","ONT8",1));
|
||||
when(warehouse.resolveLocation(92,"LOC-301")).thenReturn(new WarehouseResourceGateway.WarehouseLocationReference(301,"LOC-301","RACK",BigDecimal.TEN,"ACTIVE"));
|
||||
service=new ReturnCartonReturnService(repo,pallets,warehouse,new ObjectMapper(),new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
db.update("INSERT INTO ll_exception_order(id,customer_id,exception_no,delivery_order_id,last_mile_order_id,outbound_no,status,return_direction,overseas_warehouse_id) VALUES(1,'C','EX-1',3,100,'OUT-1','PROCESSING','OVERSEAS_WAREHOUSE','92')");
|
||||
db.update("INSERT INTO ll_delivery_order(id,customer_id,outbound_order_id) VALUES(3,'C',2)");
|
||||
db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,status) VALUES(2,'C','OUT-1',10,'OUTBOUNDED')");
|
||||
db.update("INSERT INTO ll_outbound_pallet_item(id,customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,destination_warehouse_code) VALUES(5,'C',2,500,'RL-1','ONT8')");
|
||||
db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no) VALUES(10,'C','DP-1')");
|
||||
db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,inbound_code,destination_warehouse_code,source_stage,overseas_warehouse_id) VALUES(4,'C',10,500,'RL-1','ONT8','RETURN_RELABEL','92')");
|
||||
db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id) VALUES(102,'C',10)");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(103,'C',102,4,101,1,8)");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,packages) VALUES(105,'C',103,100,1)");
|
||||
db.update("INSERT INTO ll_exception_return_item(id,customer_id,exception_order_id,outbound_pallet_item_id,return_carton_id,approved_return_packages,received_packages) VALUES(9,'C',1,5,101,1,0)");
|
||||
db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,carton_mark,status,total_quantity,relabel_application_id,relabel_no) VALUES(101,'C','92','BOX-1','OUTBOUNDED',8,88,'RL-1')");
|
||||
db.update("INSERT INTO ll_return_inventory_carton_item(id,carton_id,customer_id,sku_code,quantity,packing_reserved_quantity) VALUES(201,101,'C','SKU-A',6,0),(202,101,'C','SKU-B',2,0)");
|
||||
db.update("INSERT INTO ll_return_inventory_outbound_movement(id,outbound_pick_id,customer_id,overseas_warehouse_id,carton_id,carton_mark,outbound_no,sku_code,quantity,cartons_before,cartons_after) VALUES(301,103,'C','92',101,'BOX-1','DP-1','SKU-A',6,1,0),(302,103,'C','92',101,'BOX-1','DP-1','SKU-B',2,1,0)");
|
||||
order=map(ExceptionOrderMapper.class).selectById(1L);item=new ExceptionReturnItem();item.setId(9L);item.setOutboundPalletItemId(5L);item.setReturnCartonId(101L);item.setApprovedReturnPackages(1L);item.setReceivedPackages(0L);
|
||||
}
|
||||
/** 每个SKU件数恢复一次,原箱只计一运输箱,结案查询读取真实回仓凭据。 */
|
||||
@Test void restoresOriginalCartonAndEverySkuExactlyOnceWithoutSyntheticPositions() {
|
||||
tx.executeWithoutResult(s->assertThat(service.restore(order,List.of(item),List.of(receipt()),List.of(999L),"OP")).isEqualTo(1));
|
||||
assertThat(db.queryForList("SELECT total_quantity FROM ll_return_inventory_balance ORDER BY sku_code",Long.class)).containsExactly(6L,2L);
|
||||
assertThat(db.queryForObject("SELECT SUM(packages) FROM ll_return_carton_stock_receipt",Long.class)).isEqualTo(1);
|
||||
assertThat(db.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=101",String.class)).isEqualTo("AVAILABLE");
|
||||
assertThat(db.queryForObject("SELECT relabel_no FROM ll_return_inventory_carton WHERE id=101",String.class)).isNull();
|
||||
assertThat(map(DeliveryOperationMapper.class).returnReceipts("C",3L)).singleElement().satisfies(fact->{assertThat(fact.packages()).isEqualTo(1);assertThat(fact.dispatchItemId()).isEqualTo(4);});
|
||||
assertThatThrownBy(()->tx.executeWithoutResult(s->service.restore(order,List.of(item),List.of(receipt()),List.of(999L),"OP"))).hasMessageContaining("已回仓");
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_return_carton_stock_receipt",Long.class)).isEqualTo(2);
|
||||
}
|
||||
/** 任一SKU凭据写入失败,前一SKU余额、原箱状态和所有凭据一起回滚。 */
|
||||
@Test void failureInTheSecondSkuRollsBackAllStockAndReceiptWrites() {
|
||||
db.execute("ALTER TABLE ll_return_carton_stock_receipt ADD CONSTRAINT reject_second CHECK(sku_code<>'SKU-B')");
|
||||
assertThatThrownBy(()->tx.executeWithoutResult(s->service.restore(order,List.of(item),List.of(receipt()),List.of(999L),"OP"))).isInstanceOf(RuntimeException.class);
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_return_inventory_balance",Long.class)).isZero();
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_return_carton_stock_receipt",Long.class)).isZero();
|
||||
assertThat(db.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=101",String.class)).isEqualTo("OUTBOUNDED");
|
||||
}
|
||||
/** 其他客户和伪造出库归属不能恢复库存。 */
|
||||
@Test void anotherCustomerCannotBorrowTheOriginalCartonOrShipment() {
|
||||
order.setCustomerId("OTHER");
|
||||
assertThatThrownBy(()->tx.executeWithoutResult(s->service.restore(order,List.of(item),List.of(receipt()),List.of(999L),"OP"))).hasMessageContaining("出库及原订单");
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_return_carton_stock_receipt",Long.class)).isZero();
|
||||
}
|
||||
/** 完成命令真实落账后才推进派送并供车次结案消费,同键重放不重复恢复库存。 */
|
||||
@Test void realExceptionCompletionAndTripCompletionConsumeTheNativeReceipt() {
|
||||
db.update("UPDATE ll_exception_order SET exception_type='RETURN',delivery_no='DL-1',version=0 WHERE id=1");
|
||||
db.update("UPDATE ll_delivery_order SET delivery_no='DL-1',dispatch_order_id=10,outbound_no='OUT-1',status='RETURN_PENDING',package_count=1,version=0 WHERE id=3");
|
||||
db.update("UPDATE ll_dispatch_order SET status='DEPARTED',shipped_package_count=1,overseas_warehouse_id='92' WHERE id=10");
|
||||
db.update("UPDATE ll_outbound_order SET package_count=1 WHERE id=2");
|
||||
db.update("INSERT INTO ll_order(id,customer_id,order_no,delivery_order_no) VALUES(100,'C','LM-1','DL-1')");
|
||||
var business=business();
|
||||
var request=new ExceptionOrderRequests.Complete(0,"native-complete","实收原箱",List.of(999L),List.of(receipt()));
|
||||
var completed=tx.execute(s->business.complete("C","OP","EX-1",request));
|
||||
assertThat(completed.status()).isEqualTo("COMPLETED");assertThat(completed.restoredPackages()).isEqualTo(1);
|
||||
assertThat(db.queryForObject("SELECT status FROM ll_delivery_order WHERE id=3",String.class)).isEqualTo("RETURNED");
|
||||
assertThat(tx.execute(s->business.complete("C","OP","EX-1",request)).replayed()).isTrue();
|
||||
assertThat(db.queryForObject("SELECT SUM(packages) FROM ll_return_carton_stock_receipt",Long.class)).isEqualTo(1);
|
||||
var fleet=new com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter(map(DispatchOrderMapper.class),map(DispatchPdaTaskMapper.class),map(OutboundOrderMapper.class),map(DeliveryOrderMapper.class),map(DispatchItemMapper.class),map(DispatchPdaPickMapper.class),mock(com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService.class),map(DeliveryOperationMapper.class));
|
||||
var outcome=tx.execute(s->fleet.completion("DP-1",true));
|
||||
assertThat(outcome.blockReason()).isNull();assertThat(outcome.outcomes()).singleElement().satisfies(row->{assertThat(row.returnedPackages()).isEqualTo(1);assertThat(row.remainingPackages()).isZero();});
|
||||
}
|
||||
/** 不制造入仓单,从真实换标主单经过请求校验、标记、受理、回仓直至派送结案。 */
|
||||
@Test void nativeMasterWithoutInboundCanMarkAndCompleteAfterBeanValidation() throws Exception {
|
||||
prepareMarkReturn();
|
||||
var business=business();
|
||||
var candidates=business.returnCandidates("C","DL-1");
|
||||
assertThat(candidates).singleElement().satisfies(row->{
|
||||
assertThat(row.lastMileOrderId()).isEqualTo(100L);assertThat(row.inboundOrderId()).isNull();
|
||||
assertThat(row.returnSources()).singleElement().satisfies(source->{
|
||||
assertThat(source.returnCartonId()).isEqualTo(101L);assertThat(source.inventoryPositionId()).isNull();
|
||||
});
|
||||
});
|
||||
var json=new ObjectMapper();
|
||||
var request=json.readValue("{\"lastMileOrderId\":100,\"reason\":\"拒签原箱退回\",\"returnDirection\":\"OVERSEAS_WAREHOUSE\",\"items\":[{\"outboundPalletItemId\":5,\"returnCartonId\":101,\"packageCount\":1}],\"idempotencyKey\":\"native-mark\"}",ExceptionOrderRequests.MarkReturn.class);
|
||||
try(var factory=jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
||||
var validator=factory.getValidator();assertThat(validator.validate(request)).isEmpty();
|
||||
var missing=new ExceptionOrderRequests.MarkReturn(null,"原因","OVERSEAS_WAREHOUSE",null,request.items(),"missing");
|
||||
assertThat(validator.validate(missing)).anySatisfy(v->assertThat(v.getPropertyPath().toString()).isEqualTo("lastMileOrderId"));
|
||||
var invalid=new ExceptionOrderRequests.MarkReturn(100L,"原因","OVERSEAS_WAREHOUSE",null,List.of(new ExceptionOrderRequests.ReturnItem(5L,null,0,101L)),"invalid");
|
||||
assertThat(validator.validate(invalid)).anySatisfy(v->assertThat(v.getPropertyPath().toString()).contains("packageCount"));
|
||||
}
|
||||
var created=tx.execute(t->business.markReturn("C","OP","操作员","DL-1",request));
|
||||
assertThat(tx.execute(t->business.markReturn("C","OP","操作员","DL-1",request)).replayed()).isTrue();
|
||||
assertThat(db.queryForObject("SELECT inbound_order_id FROM ll_exception_order WHERE id=?",Long.class,created.id())).isNull();
|
||||
tx.executeWithoutResult(t->business.startProcessing("C","OP",created.exceptionNo(),new ExceptionOrderRequests.StartProcessing(0,"native-start","核实回仓")));
|
||||
var result=tx.execute(t->business.complete("C","OP",created.exceptionNo(),new ExceptionOrderRequests.Complete(1,"native-close","实收原箱",List.of(999L),List.of(receipt()))));
|
||||
assertThat(result.restoredPackages()).isEqualTo(1);assertThat(db.queryForObject("SELECT status FROM ll_delivery_order WHERE id=3",String.class)).isEqualTo("RETURNED");
|
||||
}
|
||||
/** 同一派送的两个无入仓主单不能互借原箱,伪造主单不属于候选。 */
|
||||
@Test void nativeCandidatesStayBoundToTheActualMasterWithoutInbound() {
|
||||
prepareMarkReturn();
|
||||
db.update("INSERT INTO ll_order(id,customer_id,order_no,delivery_order_no,outbound_order_no) VALUES(101,'C','LM-2','DL-1','OUT-1')");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(104,'C',102,4,102,1,1)");
|
||||
db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,packages) VALUES(106,'C',104,101,1)");
|
||||
db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,carton_mark,status,total_quantity) VALUES(102,'C','92','BOX-2','OUTBOUNDED',1)");
|
||||
var business=business();var rows=business.returnCandidates("C","DL-1");
|
||||
assertThat(rows).hasSize(2);
|
||||
assertThat(rows.get(0).returnSources()).singleElement().satisfies(source->assertThat(source.returnCartonId()).isEqualTo(101L));
|
||||
assertThat(rows.get(1).returnSources()).singleElement().satisfies(source->assertThat(source.returnCartonId()).isEqualTo(102L));
|
||||
var wrong=new ExceptionOrderRequests.MarkReturn(101L,"原因","OVERSEAS_WAREHOUSE",null,List.of(new ExceptionOrderRequests.ReturnItem(5L,null,1,101L)),"wrong-master");
|
||||
assertThatThrownBy(()->tx.execute(t->business.markReturn("C","OP","操作员","DL-1",wrong))).hasMessageContaining("不属于该派送单和尾程主单");
|
||||
var foreign=new ExceptionOrderRequests.MarkReturn(999L,"原因","OVERSEAS_WAREHOUSE",null,wrong.items(),"foreign-master");
|
||||
assertThatThrownBy(()->tx.execute(t->business.markReturn("C","OP","操作员","DL-1",foreign))).hasMessageContaining("主单不属于该派送单");
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_exception_order",Long.class)).isZero();
|
||||
}
|
||||
/** 将真实已出库换标主单置为拒签,不添加普通入仓关系。 */
|
||||
private void prepareMarkReturn() {
|
||||
db.update("DELETE FROM ll_exception_return_item");db.update("DELETE FROM ll_exception_order");
|
||||
db.update("UPDATE ll_delivery_order SET delivery_no='DL-1',dispatch_order_id=10,outbound_no='OUT-1',overseas_warehouse_id='92',status='REJECTED',package_count=1,version=0 WHERE id=3");
|
||||
db.update("INSERT INTO ll_order(id,customer_id,order_no,delivery_order_no,outbound_order_no,overseas_warehouse_id,package_count) VALUES(100,'C','LM-1','DL-1','OUT-1','92',1)");
|
||||
}
|
||||
/** 使用真实Mapper组装异常业务服务,资源和文件网关维持隔离。 */
|
||||
private ExceptionOrderService business() {
|
||||
var business=new ExceptionOrderService();
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"exceptionOrders",map(ExceptionOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"returnItems",map(ExceptionReturnItemMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"commands",map(ExceptionOrderCommandMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"deliveryOrders",map(DeliveryOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"deliveryOperations",map(DeliveryOperationMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"lastMileOrders",map(com.baiyue.lastleg.mapper.LastlegOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"objectMapper",new ObjectMapper().findAndRegisterModules());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"evidenceFiles",mock(com.baiyue.lastleg.exceptionorder.gateway.ExceptionEvidenceFileGateway.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"nativeReturns",service);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"orderNoGenerator",new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"inboundOrders",map(com.baiyue.lastleg.mapper.LastlegInboundOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"outboundOrders",map(OutboundOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"outboundPalletItems",map(OutboundPalletItemMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"outboundPdaPicks",map(OutboundPdaPickMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"dispatchItems",map(DispatchItemMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"dispatchPdaTasks",map(DispatchPdaTaskMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"dispatchPdaPicks",map(DispatchPdaPickMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"dispatchPdaPickOrders",map(DispatchPdaPickOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"returnCartons",map(ReturnInventoryCartonMapper.class));
|
||||
return business;
|
||||
}
|
||||
/** 只提交原箱来源和真实目标资源。 */
|
||||
private ExceptionOrderRequests.ReturnReceipt receipt(){return new ExceptionOrderRequests.ReturnReceipt(5L,null,1,501L,"P-NEW",301L,"LOC-301",101L);}
|
||||
/** 根据实体类型建立测试表;正式DDL另由Schema合同验证。 */
|
||||
private static String sqlType(Class<?> type){return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(8000)";}
|
||||
/** 获取隔离库的真实Mapper代理。 */
|
||||
private <T>T map(Class<T> type){return session.getMapper(type);}
|
||||
}
|
||||
+2
-2
@@ -19,12 +19,12 @@ class ReturnReceiptPolicyTest {
|
||||
.hasMessageContaining("累计实收");
|
||||
}
|
||||
@Test void wrongPositionCannotBorrowAnotherSourceApproval() {
|
||||
var wrong = new ReturnReceipt(10L, 999L, 1, 20L, "P", 30L, "L");
|
||||
var wrong = new ReturnReceipt(10L, 999L, 1, 20L, "P", 30L, "L", null);
|
||||
assertThatThrownBy(() -> ReturnReceiptPolicy.quantities(List.of(source()), List.of(wrong))).hasMessageContaining("冻结来源");
|
||||
}
|
||||
private ExceptionReturnItem source() {
|
||||
var value = new ExceptionReturnItem(); value.setId(1L); value.setOutboundPalletItemId(10L);
|
||||
value.setInventoryPositionId(11L); value.setApprovedReturnPackages(4L); value.setReceivedPackages(0L); return value;
|
||||
}
|
||||
private ReturnReceipt receipt(long amount) { return new ReturnReceipt(10L, 11L, amount, 20L, "P", 30L, "L"); }
|
||||
private ReturnReceipt receipt(long amount) { return new ReturnReceipt(10L, 11L, amount, 20L, "P", 30L, "L", null); }
|
||||
}
|
||||
|
||||
+15
@@ -33,6 +33,21 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@Isolated("Manual retry regression temporarily changes the JVM default time zone")
|
||||
class OverseasFeeCancellationPersistenceTest {
|
||||
/** 相同派送费版本重复发布只保留一个真实Outbox事件,重试不制造新应收源。 */
|
||||
@Test void deliveryFeePublicationReplayKeepsOneImmutableEvent() {
|
||||
var producer=new OverseasFeeOutboxService();
|
||||
ReflectionTestUtils.setField(producer,"mapper",events);
|
||||
ReflectionTestUtils.setField(producer,"objectMapper",new ObjectMapper().findAndRegisterModules());
|
||||
var deliveryFee=new OverseasFeeSource();org.springframework.beans.BeanUtils.copyProperties(source,deliveryFee);
|
||||
deliveryFee.setSourceFeeId(702L);deliveryFee.setSourceType("OVERSEAS_DELIVERY_FEE");deliveryFee.setSourceNo("DL-REPLAY");
|
||||
producer.publish(deliveryFee,"PUBLISHED");
|
||||
String key=OverseasFeeCancellation.eventKey(deliveryFee,"PUBLISHED");
|
||||
var first=events.findByEventKey(key);
|
||||
producer.publish(deliveryFee,"PUBLISHED");producer.publish(deliveryFee,"PUBLISHED");
|
||||
var replay=events.findByEventKey(key);
|
||||
assertThat(replay.getId()).isEqualTo(first.getId());assertThat(replay.getPayloadJson()).isEqualTo(first.getPayloadJson());
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_event_outbox WHERE event_key=?",Integer.class,key)).isEqualTo(1);
|
||||
}
|
||||
JdbcTemplate jdbc;
|
||||
LastlegEventOutboxMapper events;
|
||||
OverseasFeeSourceMapper fees;
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@ class FleetBookingFreightTest {
|
||||
final FleetBookingReservations reservations = new FleetBookingReservations(bookings, json);
|
||||
final FleetBookingFreight service = new FleetBookingFreight(bookings, bindings, reservations, json);
|
||||
FleetBookingEntity booking;
|
||||
final FleetDispatchGateway.SourceCargo source = new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 12, "FBA:ONT8");
|
||||
final FleetDispatchGateway.SourceCargo source = new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 12, "FBA:ONT8");
|
||||
@BeforeEach void setup() throws Exception {
|
||||
booking = new FleetBookingEntity();
|
||||
booking.setId(1L); booking.setTripId(2L); booking.setBookingNo("FB-1"); booking.setIncomingNo("WT-1");
|
||||
@@ -29,10 +29,10 @@ class FleetBookingFreightTest {
|
||||
booking.setReceiptJson(json.writeValueAsString(new FleetBookingModels.AcceptedReceipt(
|
||||
new FleetBookingIncomingGateway.Evidence("WT-1", "IN-1", "WH-1", "FBA:ONT8", "READY_FOR_DISPATCH", "receipt-1",
|
||||
List.of(new FleetBookingIncomingGateway.Pallet("501", "PLT-501", 12, BigDecimal.TEN, BigDecimal.ONE)), 12),
|
||||
List.of(new FleetBookingModels.PalletSpaces("501", 2)), "OP-1", Instant.parse("2026-10-01T00:00:00Z"))));
|
||||
List.of(new FleetBookingModels.PalletSpaces("501", 2, null, null)), "OP-1", Instant.parse("2026-10-01T00:00:00Z"))));
|
||||
var binding = new FleetTripDispatch(); binding.setTripId(2L); binding.setDispatchId(3L);
|
||||
when(bindings.findByDispatchNo("DP-1")).thenReturn(binding);
|
||||
when(bookings.incomingForFreight("WT-1")).thenReturn(booking);
|
||||
when(bookings.forInboundSource(anyString(), eq("IN-1"))).thenReturn(booking);
|
||||
}
|
||||
@Test void quoteUsesConfirmedActualPriceAndSeparatesOtherCargo() {
|
||||
var preview = service.preview(3, "DP-1", "C-1", List.of(source));
|
||||
@@ -53,9 +53,9 @@ class FleetBookingFreightTest {
|
||||
@Test void rejectsPartialOrChangedDestinationAndDetectsNewQuoteVersion() {
|
||||
var before = service.preview(3, "DP-1", "C-1", List.of(source));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preview(3, "DP-1", "C-1", List.of(
|
||||
new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 11, "FBA:ONT8"))));
|
||||
new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 11, "FBA:ONT8"))));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preview(3, "DP-1", "C-1", List.of(
|
||||
new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 12, "FBA:LAX9"))));
|
||||
new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 12, "FBA:LAX9"))));
|
||||
booking.setRecordVersion(5L);
|
||||
assertNotEquals(before.snapshotHash(), service.preview(3, "DP-1", "C-1", List.of(source)).snapshotHash());
|
||||
}
|
||||
|
||||
+73
-4
@@ -127,13 +127,18 @@ class FleetTripPersistenceTest {
|
||||
for (String table : List.of("ll_fleet_driver", "ll_fleet_driver_history", "ll_vehicle", "ll_fleet_trip", "ll_fleet_resource_assignment", "ll_fleet_trip_history",
|
||||
"ll_fleet_capacity_offer", "ll_fleet_capacity_offer_command", "ll_fleet_external_booking", "ll_fleet_trip_dispatch", "ll_dispatch_order", "ll_dispatch_pda_task", "ll_outbound_order", "ll_delivery_order",
|
||||
"ll_dispatch_item", "ll_dispatch_pda_pick", "ll_dispatch_pda_operation", "ll_dispatch_pallet_lock", "ll_delivery_operation",
|
||||
"ll_outbound_pallet_item", "ll_exception_order", "ll_exception_return_item", "ll_inventory_ledger", "ll_dispatch_pda_pick_order")) {
|
||||
"ll_outbound_pallet_item", "ll_return_carton_stock_receipt", "ll_exception_order", "ll_exception_return_item", "ll_inventory_ledger", "ll_dispatch_pda_pick_order")) {
|
||||
var match = Pattern.compile("(?s)CREATE TABLE(?: IF NOT EXISTS)? " + table + " \\(.*?;").matcher(schema);
|
||||
assertThat(match.find()).as(table).isTrue();
|
||||
jdbc.execute(match.group().replace(" JSON", " LONGTEXT")
|
||||
jdbc.execute(match.group().replace(" JSON", " LONGTEXT").replaceAll("(?i) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", "")
|
||||
.replaceAll("\\((receipt_id|return_receipt_id|return_carton_id) IS NOT NULL\\)", "(CASE WHEN $1 IS NOT NULL THEN 1 ELSE 0 END)"));
|
||||
}
|
||||
jdbc.execute("CREATE TABLE ll_warehouse_transfer_order(customer_id VARCHAR(64),transfer_no VARCHAR(64),inbound_code VARCHAR(64))");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(64),pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),balance_id BIGINT,available_packages BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(64),inbound_order_id BIGINT,inbound_code VARCHAR(64))");
|
||||
jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),source_region VARCHAR(32),source_inbound_id VARCHAR(64))");
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.addMapper(com.baiyue.lastleg.delivery.mapper.DispatchFeeCargoMapper.class);
|
||||
for (var mapper : List.of(DriverMapper.class, DriverHistoryMapper.class, VehicleMapper.class, FleetTripMapper.class, FleetResourceMapper.class,
|
||||
FleetTripHistoryMapper.class, FleetTripDispatchMapper.class, FleetBookingMapper.class, FleetCapacityOfferMapper.class, FleetCapacityOfferCommandMapper.class, DispatchOrderMapper.class, DispatchPdaTaskMapper.class, OutboundOrderMapper.class, DeliveryOrderMapper.class,
|
||||
DispatchItemMapper.class, DispatchPdaPickMapper.class, DispatchPdaOperationMapper.class, DispatchPalletLockMapper.class, DeliveryOperationMapper.class)) configuration.addMapper(mapper);
|
||||
@@ -161,6 +166,9 @@ class FleetTripPersistenceTest {
|
||||
var bindingMapper = session.getMapper(FleetTripDispatchMapper.class);
|
||||
bindingGuard = transactional(new FleetDispatchBindingGuard(bindingMapper, json), FleetDispatchBindingGuard.class);
|
||||
var workflow = new DispatchPdaWorkflowService();
|
||||
var deliveryExecution = new com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(
|
||||
session.getMapper(OutboundOrderMapper.class), session.getMapper(DeliveryOrderMapper.class)));
|
||||
ReflectionTestUtils.setField(workflow, "deliveryExecution", deliveryExecution);
|
||||
ReflectionTestUtils.setField(workflow, "orders", session.getMapper(DispatchOrderMapper.class));
|
||||
ReflectionTestUtils.setField(workflow, "outboundOrders", session.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(workflow, "items", session.getMapper(DispatchItemMapper.class));
|
||||
@@ -175,6 +183,9 @@ class FleetTripPersistenceTest {
|
||||
var dispatchGateway = transactional(new DispatchFleetAdapter(session.getMapper(DispatchOrderMapper.class), session.getMapper(DispatchPdaTaskMapper.class),
|
||||
session.getMapper(OutboundOrderMapper.class), session.getMapper(DeliveryOrderMapper.class), session.getMapper(DispatchItemMapper.class), session.getMapper(DispatchPdaPickMapper.class),
|
||||
transactional(workflow, DispatchPdaWorkflowService.class), session.getMapper(DeliveryOperationMapper.class)), DispatchFleetAdapter.class);
|
||||
ReflectionTestUtils.setField(dispatchGateway, "deliveryExecution", deliveryExecution);
|
||||
ReflectionTestUtils.setField(dispatchGateway, "cargoSources", new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(session.getMapper(com.baiyue.lastleg.delivery.mapper.DispatchFeeCargoMapper.class))));
|
||||
service = transactional(new FleetTripService(session.getMapper(FleetTripMapper.class), resources, session.getMapper(FleetTripHistoryMapper.class), vehicles, drivers, directory, json,
|
||||
bindingMapper, dispatchGateway, reservations), FleetTripService.class);
|
||||
preparation = transactional(new FleetTripPreparationService(service,session.getMapper(FleetTripMapper.class),session.getMapper(FleetCapacityOfferCommandMapper.class),json),FleetTripPreparationService.class);
|
||||
@@ -530,6 +541,10 @@ class FleetTripPersistenceTest {
|
||||
var confirmed=bookingService.confirm(number,draft.booking().bookingNo(),"1",new FleetBookingModels.Command(draft.tripRecordVersion(),0L,"receipt-reserve",null));
|
||||
var awaiting=new FleetBookingIncomingGateway.Evidence("WT-EXT","WT-EXT","92","FBA:ONT8","WAIT_PDA_RECEIVING","empty",List.of(),0);
|
||||
when(incoming.create(eq("1"),any())).thenReturn(awaiting);
|
||||
jdbc.update("INSERT INTO ll_warehouse_transfer_order VALUES('9','WT-EXT','WT-EXT')");
|
||||
jdbc.update("INSERT INTO ll_inventory_position VALUES(9001,'9',902,'92',9002,22)");
|
||||
jdbc.update("INSERT INTO ll_inventory_balance VALUES(9002,'9',9003,'WT-EXT')");
|
||||
jdbc.update("INSERT INTO ll_order VALUES(9003,'9','WAREHOUSE_TRANSFER',NULL)");
|
||||
var request=new FleetBookingModels.StartIncoming(confirmed.tripRecordVersion(),confirmed.booking().recordVersion(),departure.minusSeconds(120),null,null,null,null,null,"incoming-start");
|
||||
var started=bookingService.startIncoming(number,draft.booking().bookingNo(),"1",request);
|
||||
assertThat(bookingService.startIncoming(number,draft.booking().bookingNo(),"1",request).replayed()).isTrue();
|
||||
@@ -540,11 +555,11 @@ class FleetTripPersistenceTest {
|
||||
when(incoming.inspect("9","WT-EXT",true)).thenReturn(actual);
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(actual);
|
||||
assertThatThrownBy(()->bookingService.acceptReceipt(number,draft.booking().bookingNo(),"1",new FleetBookingModels.AcceptReceipt(started.tripRecordVersion(),started.booking().recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1)),new BigDecimal("55.00"),"accept-receipt"))).hasMessageContaining("实收确认未完成");
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt"))).hasMessageContaining("实收确认未完成");
|
||||
assertThat(bookingService.list(number).records().get(0).agreedAmount()).isEqualByComparingTo("100.00");
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(new FleetBookingIncomingGateway.Evidence(actual.incomingNo(),actual.inboundCode(),actual.warehouseId(),actual.destinationKey(),"READY_FOR_DISPATCH",actual.receiptHash(),actual.pallets(),actual.receivedPackages()));
|
||||
var accepted=bookingService.acceptReceipt(number,draft.booking().bookingNo(),"1",new FleetBookingModels.AcceptReceipt(started.tripRecordVersion(),started.booking().recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1)),new BigDecimal("55.00"),"accept-receipt"));
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt"));
|
||||
assertThat(accepted.booking().status()).isEqualTo("RECEIVED");assertThat(accepted.booking().cargo().packageCount()).isEqualTo(22);
|
||||
assertThat(accepted.booking().agreedAmount()).isEqualByComparingTo("55.00");
|
||||
assertThat(accepted.booking().quoteSource()).isEqualTo(source);
|
||||
@@ -568,6 +583,33 @@ class FleetTripPersistenceTest {
|
||||
service.unbindDispatch(number,"1",new FleetTripModels.BindingCommand(planned.recordVersion(),"DP-13","撤销未开工关联"));
|
||||
assertThat(bookingService.list(number).records().get(0).status()).isEqualTo("RECEIVED");
|
||||
assertThat(bookingService.list(number).records().get(0).dispatchNo()).isNull();
|
||||
// 解除关联后重新核对已有实收,不创建新库存,也不重复预留承运容量。
|
||||
var reopenedPlan=execution.capacityPlan(number);
|
||||
var refreshed=execution.confirmCapacityPlan(number,"1",new FleetExecutionModels.Confirm(
|
||||
reopenedPlan.recordVersion(),reopenedPlan.manifestHash(),List.of(measurement()),"解除关联后重新核对"));
|
||||
var currentBooking=bookingService.list(number).records().get(0);
|
||||
var receiptAgain=new FleetBookingModels.AcceptReceipt(refreshed.recordVersion(),currentBooking.recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt-again");
|
||||
var renewed=bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",receiptAgain);
|
||||
assertThat(renewed.booking().status()).isEqualTo("RECEIVED");
|
||||
assertThat(renewed.booking().cargo().packageCount()).isEqualTo(22);
|
||||
assertThat(bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",receiptAgain).replayed()).isTrue();
|
||||
assertThat(execution.capacityPlan(number).availableSegments().get(0).remaining().palletSpaces()).isEqualTo(28);
|
||||
// 实物换托后源摘要变化且旧测量失效;缺测量拒绝,显式实测后更新快照及容量。
|
||||
var moved=new FleetBookingIncomingGateway.Evidence("WT-EXT","WT-EXT","92","FBA:ONT8","READY_FOR_DISPATCH","moved-receipt",
|
||||
List.of(new FleetBookingIncomingGateway.Pallet("903","PALLET-903",22,null,null)),22);
|
||||
when(incoming.inspect("9","WT-EXT",true)).thenReturn(moved);
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(moved);
|
||||
var withoutMeasure=new FleetBookingModels.AcceptReceipt(renewed.tripRecordVersion(),renewed.booking().recordVersion(),moved.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("903",1,null,null)),new BigDecimal("55.00"),"moved-confirm");
|
||||
assertThatThrownBy(()->bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withoutMeasure)).hasMessageContaining("实际托盘");
|
||||
var withMeasure=new FleetBookingModels.AcceptReceipt(renewed.tripRecordVersion(),renewed.booking().recordVersion(),moved.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("903",1,new BigDecimal("170"),new BigDecimal("1.7"))),new BigDecimal("55.00"),"moved-confirm");
|
||||
var measuredAgain=bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withMeasure);
|
||||
assertThat(measuredAgain.booking().cargo().weightKg()).isEqualByComparingTo("170");
|
||||
assertThat(measuredAgain.booking().cargo().volumeCbm()).isEqualByComparingTo("1.7");
|
||||
assertThat(measuredAgain.booking().receipt().source().pallets()).singleElement().satisfies(p->{assertThat(p.warehousePalletId()).isEqualTo("903");assertThat(p.weightKg()).isEqualByComparingTo("170");});
|
||||
assertThat(bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withMeasure).replayed()).isTrue();
|
||||
}
|
||||
|
||||
@Test void externalInquiryDoesNotReserveUntilConfirmedAndCancellationReleasesExactlyOnce() {
|
||||
@@ -931,6 +973,31 @@ class FleetTripPersistenceTest {
|
||||
} finally { executor.shutdownNow(); }
|
||||
}
|
||||
|
||||
/** 取消与实际整车发车并发时,已拣货车只能发车;出库和派送不得产生取消终态。 */
|
||||
@Test void outboundCancellationRacingDepartureCannotCreateDepartedCancelledCargo() throws Exception {
|
||||
String number=readyTrip();var loaded=confirm(number);
|
||||
var cancel=new com.baiyue.lastleg.delivery.service.DeliveryTransferService();
|
||||
var config=new MybatisConfiguration();config.addMapper(OutboundOrderMapper.class);config.addMapper(DispatchOrderMapper.class);
|
||||
var source=(javax.sql.DataSource)jdbc.getDataSource();
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(source);factory.setConfiguration(config);factory.afterPropertiesSet();
|
||||
var data=new SqlSessionTemplate(factory.getObject());
|
||||
ReflectionTestUtils.setField(cancel,"outboundOrders",data.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(cancel,"dispatchOrders",data.getMapper(DispatchOrderMapper.class));
|
||||
ReflectionTestUtils.setField(cancel,"fleetBindings",bindingGuard);
|
||||
var start=new CountDownLatch(1);var executor=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var departure=executor.submit(()->{start.await(5,TimeUnit.SECONDS);return execution.depart("92","DP-11","1",new FleetExecutionModels.Depart(loaded.recordVersion(),"race-depart","并发取消验证"));});
|
||||
var cancellation=executor.submit(()->{start.await(5,TimeUnit.SECONDS);try{
|
||||
new org.springframework.transaction.support.TransactionTemplate(transactions).executeWithoutResult(s->cancel.cancelOutbound("OUT-11","1",new com.baiyue.lastleg.delivery.dto.DeliveryRequests.Cancel("并发取消")));
|
||||
return false;
|
||||
}catch(ResponseStatusException rejected){return rejected.getStatusCode().value()==409;}});
|
||||
start.countDown();assertThat(departure.get(20,TimeUnit.SECONDS).shippedPackages()).isEqualTo(20);assertThat(cancellation.get(20,TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(jdbc.queryForList("SELECT status FROM ll_outbound_order",String.class)).containsOnly("OUTBOUNDED");
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_delivery_order WHERE status='CANCELLED'",Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pda_operation WHERE operation_type='DEPART'",Integer.class)).isEqualTo(2);
|
||||
} finally {executor.shutdownNow();}
|
||||
}
|
||||
|
||||
@Test void changingAssignedDriverToDomesticBlocksPreviewAndDepartureUntilCorrected() {
|
||||
String number = readyTrip(); var confirmed = confirm(number);
|
||||
long driverId = Long.parseLong(service.detail(number).trip().driverId());
|
||||
@@ -1125,6 +1192,8 @@ class FleetTripPersistenceTest {
|
||||
dispatchId + 1000, customer, "OUT-" + dispatchId, dispatchId, "out-key-" + dispatchId);
|
||||
jdbc.update("INSERT INTO ll_delivery_order(id,customer_id,delivery_no,outbound_order_id,outbound_no,dispatch_order_id,delivery_group_key,idempotency_key,expected_arrival_at) VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
dispatchId + 2000, customer, "DL-" + dispatchId, dispatchId + 1000, "OUT-" + dispatchId, dispatchId, "DEST", "dl-key-" + dispatchId, LocalDateTime.of(2030, 1, 1, 12, 0));
|
||||
jdbc.update("UPDATE ll_outbound_order SET package_count=10 WHERE id=?", dispatchId + 1000);
|
||||
jdbc.update("UPDATE ll_delivery_order SET package_count=10 WHERE id=?", dispatchId + 2000);
|
||||
}
|
||||
|
||||
private boolean attempt(CountDownLatch start, String number, long vehicle, long driver) throws Exception {
|
||||
|
||||
+2
-1
@@ -54,7 +54,8 @@ class ReturnInventoryMovementSqlTest {
|
||||
var boundSql = configuration.getMappedStatement(ReturnInventoryMovementMapper.class.getName() + "." + method)
|
||||
.getBoundSql(params);
|
||||
String sql = boundSql.getSql();
|
||||
assertEquals(7, sql.split("UNION ALL", -1).length);
|
||||
assertEquals(8, sql.split("UNION ALL", -1).length);
|
||||
assertTrue(sql.contains("ll_return_carton_stock_receipt"));
|
||||
assertTrue(!sql.contains("CONVERT("));
|
||||
assertTrue(sql.contains("m.customerId=?"));
|
||||
assertTrue(sql.contains("m.overseasWarehouseId=?"));
|
||||
|
||||
+28
@@ -13,6 +13,34 @@ import java.util.List;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
class FleetIncomingAdapterTest {
|
||||
/** 真实组成拆成新托后旧重量体积失效;其他客户库存不进入该票实收。 */
|
||||
@Test void splitPalletsRefreshTheActualReceiptAndInvalidateOldMeasurements() throws Exception {
|
||||
var data=new org.springframework.jdbc.datasource.DriverManagerDataSource("jdbc:h2:mem:"+java.util.UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
var db=new org.springframework.jdbc.core.JdbcTemplate(data);
|
||||
db.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(64),inbound_code VARCHAR(64))");
|
||||
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,balance_id BIGINT,customer_id VARCHAR(64),pallet_id BIGINT,pallet_no VARCHAR(64),overseas_warehouse_id VARCHAR(64),available_packages BIGINT,picked_packages BIGINT)");
|
||||
db.update("INSERT INTO ll_inventory_balance VALUES(1,'C','IN-1'),(2,'OTHER','IN-1')");
|
||||
db.update("INSERT INTO ll_inventory_position VALUES(10,1,'C',501,'P-501','92',5,0),(11,2,'OTHER',999,'P-999','92',99,0)");
|
||||
var config=new com.baomidou.mybatisplus.core.MybatisConfiguration();config.addMapper(com.baiyue.lastleg.transfer.mapper.FleetIncomingActualSourceMapper.class);
|
||||
var factory=new com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet();
|
||||
var session=new org.mybatis.spring.SqlSessionTemplate(factory.getObject());
|
||||
var service=mock(WarehouseTransferService.class);var orders=mock(WarehouseTransferOrderMapper.class);
|
||||
var adapter=new FleetIncomingAdapter(service,orders,new ObjectMapper());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(adapter,"actualSources",new com.baiyue.lastleg.transfer.repository.FleetIncomingActualSourceRepository(session.getMapper(com.baiyue.lastleg.transfer.mapper.FleetIncomingActualSourceMapper.class)));
|
||||
when(orders.findNo("C","WT-1")).thenReturn(new com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder());
|
||||
var detail=mock(WarehouseTransferResponses.Detail.class);when(detail.transferNo()).thenReturn("WT-1");when(detail.inboundCode()).thenReturn("IN-1");when(detail.operatingWarehouseId()).thenReturn("92");when(detail.destinationType()).thenReturn("FBA");when(detail.destinationWarehouseCode()).thenReturn("ONT8");when(detail.status()).thenReturn("READY_FOR_DISPATCH");
|
||||
var prior=mock(WarehouseTransferResponses.Pallet.class);when(prior.status()).thenReturn("COMPLETED");when(prior.warehousePalletId()).thenReturn(501L);when(prior.palletNo()).thenReturn("P-501");when(prior.cartonCount()).thenReturn(5);when(prior.grossWeightKg()).thenReturn(BigDecimal.TEN);when(prior.volumeCbm()).thenReturn(BigDecimal.ONE);when(detail.pallets()).thenReturn(List.of(prior));
|
||||
when(service.detail("C","WT-1")).thenReturn(detail);
|
||||
var before=adapter.inspect("C","WT-1",false);assertThat(before.pallets()).singleElement().satisfies(p->assertThat(p.weightKg()).isEqualByComparingTo("10"));
|
||||
db.update("UPDATE ll_inventory_position SET available_packages=0 WHERE id=10");
|
||||
db.update("INSERT INTO ll_inventory_position VALUES(12,1,'C',502,'P-502','92',2,0),(13,1,'C',503,'P-503','92',3,0)");
|
||||
var after=adapter.inspect("C","WT-1",false);
|
||||
assertThat(after.receivedPackages()).isEqualTo(5);assertThat(after.receiptHash()).isNotEqualTo(before.receiptHash());
|
||||
assertThat(after.pallets()).extracting(FleetBookingIncomingGateway.Pallet::warehousePalletId).containsExactly("502","503");
|
||||
assertThat(after.pallets()).allSatisfy(p->{assertThat(p.weightKg()).isNull();assertThat(p.volumeCbm()).isNull();});
|
||||
db.update("UPDATE ll_inventory_position SET available_packages=1 WHERE id=13");
|
||||
assertThatThrownBy(()->adapter.inspect("C","WT-1",false)).hasMessageContaining("原实收量不一致");
|
||||
}
|
||||
@Test void createsRealCustomerDeliveryWithoutClaimingStockWasReceived() {
|
||||
var service=mock(WarehouseTransferService.class);var adapter=new FleetIncomingAdapter(service,mock(WarehouseTransferOrderMapper.class),new ObjectMapper());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(adapter,"fleetPool",mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge.class));
|
||||
|
||||
Reference in New Issue
Block a user