This commit is contained in:
2026-10-19 17:56:36 +08:00
parent bb917c954c
commit dcfba1053c
232 changed files with 15655 additions and 6005 deletions
@@ -0,0 +1,120 @@
package com.baiyue.lastleg.delivery;
import com.baiyue.lastleg.delivery.entity.*;
import com.baiyue.lastleg.delivery.model.*;
import com.baiyue.lastleg.delivery.repository.DispatchDeliveryRepository;
import com.baiyue.lastleg.delivery.service.*;
import com.baiyue.lastleg.dispatch.port.*;
import com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
import com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*;
import com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.fleet.trip.FleetBookingFreight;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.*;
import static com.baiyue.lastleg.delivery.DeliveryDispatchFixtures.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** 实际整车服务配合领域策略验证分组、数组、锁点及未知结果恢复。 */
class DispatchDeliveryServiceTest {
private final DispatchExecutionPort execution=mock(DispatchExecutionPort.class);
private final DispatchDeliveryRepository repository=mock(DispatchDeliveryRepository.class);
private final DispatchDeliveryFees fees=mock(DispatchDeliveryFees.class);
private final ObjectMapper json=new ObjectMapper().findAndRegisterModules();
private final DeliveryCargoGroups grouping=new DeliveryCargoGroups(json);
private final Actor actor=new Actor("OP-A",ActorMode.PLATFORM,null);
private final List<OutboundOrder> outbounds=new ArrayList<>();
private final List<DeliveryOrder> deliveries=new ArrayList<>();
private final List<String> calls=new ArrayList<>();
private DispatchDeliveryService service;
private Context context;
private DispatchDeliveryArrangement arrangement;
/** 保存真实领域结果,持久化回滚另由Mapper事务测试负责。 */
@BeforeEach void prepare() {
context=context(List.of(cargo(1,"101","ONT8",51,2),cargo(2,"101","LAX9",52,3),cargo(3,"102","ONT8",53,4)),false);
var numbers=mock(LastlegOrderNoGenerator.class);
when(numbers.next(anyString(),anyString(),anyLong())).thenAnswer(a->a.getArgument(0)+"-"+a.getArgument(2));
service=new DispatchDeliveryService(execution,repository,grouping,fees,new NormalDeliveryCompletionPolicy(grouping),numbers,json);
when(execution.lockForArrangement(any(),eq("DP-10"))).thenAnswer(a->context);
when(execution.replay(any(),any(),any())).thenReturn(Optional.empty());
when(repository.arrangement(eq(10L),anyBoolean())).thenAnswer(a->arrangement);
when(repository.outbounds(eq(10L),anyBoolean())).thenAnswer(a->List.copyOf(outbounds));
when(repository.deliveries(eq(10L),anyBoolean())).thenAnswer(a->List.copyOf(deliveries));
doAnswer(a->{outbounds.add(a.getArgument(0));calls.add("OUTBOUND");return null;}).when(repository).create(any(OutboundOrder.class));
doAnswer(a->{deliveries.add(a.getArgument(0));calls.add("DELIVERY");return null;}).when(repository).create(any(DeliveryOrder.class));
doAnswer(a->{arrangement=a.getArgument(0);return null;}).when(repository).create(any(DispatchDeliveryArrangement.class));
when(repository.lockSource(anyString(),anyLong())).thenAnswer(a->{calls.add("SOURCE");var o=new LastlegOrder();o.setId(a.getArgument(1));
o.setCustomerId(a.getArgument(0));o.setCurrentStatus("INBOUND_COMPLETED");return o;});
when(fees.external(any())).thenReturn(Map.of("101",freight(),"102",freight()));
when(execution.recordArrangement(any(),any(),any(),any(),anyBoolean())).thenAnswer(a->{ArrangementFact f=a.getArgument(3);CommandIdentity i=a.getArgument(2);
return new CommandResult("DP-10",Action.ARRANGE_DELIVERY,i.idempotencyKey(),4,Status.ACTIVE,Stage.AFTER_LOADING,DEPARTED,
a.getArgument(4)?Effect.EXISTING:Effect.APPLIED,context.counts(),f.result(),false,a.getArgument(4));});
}
/** 两客户三个目的地形成两出库三派送,全部子单先于任何来源锁。 */
@Test void createsCompleteCustomerArraysAndLocksSourcesOnlyAfterEveryChild() {
var result=service.arrange(actor,"DP-10",request("KEY-1"));
var payload=(ArrangementResult)result.result();
assertEquals(2,payload.outbounds().size());assertEquals(3,payload.deliveries().size());
assertEquals(9,payload.deliveries().stream().mapToLong(DeliveryReference::packageCount).sum());
assertEquals(2,payload.deliveries().stream().filter(d->d.destination().warehouseCode().equals("ONT8")).map(DeliveryReference::customerId).distinct().count());
assertTrue(calls.indexOf("SOURCE")>calls.lastIndexOf("DELIVERY"));
assertTrue(outbounds.stream().allMatch(o->"WAIT_OUTBOUND".equals(o.getStatus())));
var items=org.mockito.ArgumentCaptor.forClass(OutboundPalletItem.class);verify(repository,times(3)).create(items.capture());
assertEquals(java.util.Set.of(1L,2L,3L),items.getAllValues().stream().map(OutboundPalletItem::getSourceAttributionId).collect(java.util.stream.Collectors.toSet()));
assertTrue(items.getAllValues().stream().allMatch(i->i.getSourceOrderId()!=null&&i.getSourceInventoryPositionId()!=null&&i.getDispatchItemId()!=null));
verify(fees,times(3)).publish(any(),any(),any(),any(),isNull(),any(),eq("OP-A"),anyString());
}
/** 原命令恢复先于新阶段和版本判断,不能再次写任何客户单据。 */
@Test void strictReplayDelegatesToAuthoritativeLedgerBeforeCreatingDocuments() {
var snapshot=new CommandResult("DP-10",Action.ARRANGE_DELIVERY,"KEY-1",4,Status.ACTIVE,Stage.AFTER_LOADING,
DEPARTED,Effect.APPLIED,context.counts(),null,true,false);
when(execution.replay(any(),any(),any())).thenReturn(Optional.of(snapshot));
assertSame(snapshot,service.arrange(actor,"DP-10",request("KEY-1")));
verify(execution,never()).requireFreshArrangement(any(),anyLong());
verifyNoInteractions(repository,fees);
}
/** 少一客户、错组或过期货物摘要均拒绝,不产生部分单据。 */
@Test void incompleteGroupSetAndChangedCargoCannotCreatePartialTruck() {
var full=request("KEY-1");
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.arrange(actor,"DP-10",
new ArrangeDelivery(3L,"KEY-1",full.cargoHash(),full.loadingCompletionHash(),full.groups().subList(0,2))));
assertTrue(outbounds.isEmpty());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.arrange(actor,"DP-10",
new ArrangeDelivery(3L,"KEY-2","old-hash",full.loadingCompletionHash(),full.groups())));
assertTrue(deliveries.isEmpty());
}
/** 发车只更改单据,所有客户仍共享调用方事务,不提供第二次库存扣减入口。 */
@Test void departureMarksEveryCustomerDocumentWithoutNewFeePublication() {
service.arrange(actor,"DP-10",request("KEY-1"));clearInvocations(fees);
var result=service.markDeparted(context,actor,DEPARTED,"DEPART-1");
assertEquals(9,result.shippedPackages());assertEquals(2,result.outbounds().size());
assertTrue(outbounds.stream().allMatch(o->"OUTBOUNDED".equals(o.getStatus())));
assertTrue(deliveries.stream().allMatch(d->"DELIVERING".equals(d.getStatus())&&d.getDispatchedAt()!=null));
verifyNoInteractions(fees);
}
/** 使用主流程已冻结的Long版本,缺字段不得被当成有效零。 */
@Test void publicVersionRequiresPresenceAndAcceptsExplicitZero() {
assertThrows(IllegalArgumentException.class,()->new ArrangeDelivery(null,"K","cargo","load",List.of()));
assertThrows(IllegalArgumentException.class,()->new ArrangeDelivery(-1L,"K","cargo","load",List.of()));
assertEquals(Long.valueOf(0),new ArrangeDelivery(0L,"K","cargo","load",List.of()).recordVersion());
}
/** 请求依照服务端实际共享DTO构造,不给客户或操作者字段。 */
private ArrangeDelivery request(String key) {
return new ArrangeDelivery(3L,key,"cargo-hash","loading-hash",grouping.groups(context).stream().map(g->new ArrangeGroup(g.key(),g.cargoHash(),
DEPARTED.plusSeconds(3600),"TRUCK",null,null,"WAREHOUSE_PICKUP","PALLET",null,null,null,null,null,"freight",g.cargoHash(),List.of(),List.of())).toList());
}
/** 本例费用域独立校验,外接报价的实际来源规则另有直接测试。 */
private FleetBookingFreight.Preview freight() { return new FleetBookingFreight.Preview("freight",true,List.of(),List.of()); }
}