perf(lastleg): batch live appointment work facts and filter candidates

This commit is contained in:
2026-10-15 19:10:57 +08:00
parent abf4e45e39
commit 097bec33b1
24 changed files with 441 additions and 40 deletions
@@ -0,0 +1,32 @@
package com.baiyue.lastleg.appointmentplanning;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import java.util.*;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class AppointmentCargoVerificationBatchTest {
@Test void inspectionUsesTwoBoundedBatchesAndRetainsPerSourcePrerequisitesAndAllocations() {
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentCargoVerificationMapper.class);
var service=new AppointmentCargoVerificationService(sources,mapper,new ObjectMapper());
var refs=IntStream.rangeClosed(1,200).mapToObj(i->new AppointmentCoverageService.SourceRef("C","ORDER:"+i,"FBA:ONT8")).toList();
when(sources.inspectBatch(anyList())).thenAnswer(call->call.<List<AppointmentCoverageService.SourceRef>>getArgument(0).stream().map(ref->
ref.sourceKey().equals("ORDER:2")?new AppointmentSourceService.Resolution(ref,null,new ResponseStatusException(HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中")):
new AppointmentSourceService.Resolution(ref,new AppointmentSourceService.Source(ref.sourceKey(),ref.customerId(),Long.valueOf(ref.sourceKey().substring(6)),"IN",null,"CONTAINER","100","仓",ref.destinationKey(),"ONT8",10,"ORDER:1",true,List.of()),null)).toList());
when(mapper.allocatedBatch(anyList())).thenAnswer(call->call.<List<AppointmentCoverageService.SourceRef>>getArgument(0).stream().filter(r->r.sourceKey().equals("ORDER:1"))
.map(r->new AppointmentCargoVerificationMapper.SourceAllocation(r.customerId(),r.sourceKey(),4,0)).toList());
var result=service.inspectBatch(refs);
assertEquals(200,result.size());assertEquals(6L,result.get(0).requireContext().availablePackages());
assertEquals("接仓点不在启用的美国仓目录中",assertThrows(ResponseStatusException.class,()->result.get(1).requireContext()).getReason());
assertEquals(10L,result.get(199).requireContext().availablePackages());
verify(sources,times(2)).inspectBatch(argThat(batch->batch.size()==100));
verify(mapper,times(2)).latestBatch(anyList());verify(mapper,times(2)).allocatedBatch(anyList());verifyNoMoreInteractions(mapper,sources);
when(sources.inspectBatch(anyList())).thenThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"目录不可用"));
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.inspectBatch(refs)).getStatusCode().value());
}
}
@@ -31,6 +31,7 @@ class AppointmentCoverageMapperTest {
s.execute("INSERT INTO ll_appointment_cargo_verification(id,customer_id,source_key,destination_key,version,package_count,shipment_references_json,operator_id,command_key,request_hash,created_at) VALUES(1,'A','ORDER:1','FBA:ONT8',1,19,'[]','operator','one',REPEAT('a',64),CURRENT_TIMESTAMP),(2,'A','ORDER:1','FBA:ONT8',2,20,'[\"FBA-A\"]','operator','two',REPEAT('b',64),CURRENT_TIMESTAMP),(3,'B','ORDER:1','FBA:ONT8',1,8,'[]','operator','one',REPEAT('c',64),CURRENT_TIMESTAMP)");
}
var config=new Configuration(new Environment("test",new JdbcTransactionFactory(),ds));config.setMapUnderscoreToCamelCase(true);config.addMapper(AppointmentCoverageMapper.class);
config.addMapper(AppointmentCargoVerificationMapper.class);
try(var session=new SqlSessionFactoryBuilder().build(config).openSession(true)) {
var mapper=session.getMapper(AppointmentCoverageMapper.class);
var source=mapper.sources(List.of(new AppointmentCoverageMapper.OrderRef("A",1L))).get(0);assertFalse(source.shipmentReferencesMissing());assertEquals(20,source.packages());
@@ -42,6 +43,14 @@ class AppointmentCoverageMapperTest {
assertTrue(mapper.sources(List.of(new AppointmentCoverageMapper.OrderRef("B",2L))).get(0).shipmentReferencesMissing());
var evidence=mapper.verifications(List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8")));
assertEquals(1,evidence.size());assertEquals(2,evidence.get(0).getVersion());assertEquals(20,evidence.get(0).getPackageCount());
var cargo=session.getMapper(AppointmentCargoVerificationMapper.class);
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"),new AppointmentCoverageService.SourceRef("B","ORDER:1","FBA:ONT8"),new AppointmentCoverageService.SourceRef("D","BOOKING:B-1","FBA:ONT8"));
var latest=cargo.latestBatch(refs);assertEquals(2,latest.size());
assertEquals(2,latest.stream().filter(r->r.getCustomerId().equals("A")).findFirst().orElseThrow().getVersion());
assertEquals(1,latest.stream().filter(r->r.getCustomerId().equals("B")).findFirst().orElseThrow().getVersion());
var allocations=cargo.allocatedBatch(refs);assertEquals(2,allocations.size());
assertEquals(new AppointmentCargoVerificationMapper.SourceAllocation("A","ORDER:1",20,0),allocations.stream().filter(r->r.customerId().equals("A")).findFirst().orElseThrow());
assertEquals(new AppointmentCargoVerificationMapper.SourceAllocation("D","BOOKING:B-1",0,1),allocations.stream().filter(r->r.customerId().equals("D")).findFirst().orElseThrow());
}
}
}
@@ -25,6 +25,14 @@ class AppointmentSourceServiceTest {
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"),new AppointmentCoverageService.SourceRef("B","ORDER:2","FBA:ONT8"));
var result=service.resolveBatch(refs);assertEquals(2,result.size());assertEquals(20,result.get(1).packages());
verify(orders,times(1)).selectBatchIds(anyCollection());verify(shipping,times(1)).selectBatchIds(anyCollection());verify(warehouse,times(1)).catalog("token");verify(gateway,times(1)).resolveOrders(anyList());verifyNoMoreInteractions(gateway);
b.setOverseasWarehouseId("unknown");
var inspected=service.inspectBatch(refs);assertNotNull(inspected.get(0).requireSource());
assertEquals("接仓点不在启用的美国仓目录中",inspected.get(1).error().getReason());
assertThrows(ResponseStatusException.class,()->service.resolveBatch(refs));
var unauthorized=service.inspectBatch(List.of(new AppointmentCoverageService.SourceRef("OTHER","ORDER:1","FBA:ONT8")));
assertEquals(404,assertThrows(ResponseStatusException.class,()->unauthorized.get(0).requireSource()).getStatusCode().value());
when(warehouse.catalog("token")).thenThrow(new IllegalStateException("offline"));
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.inspectBatch(refs)).getStatusCode().value());
assertThrows(ResponseStatusException.class,()->service.resolveBatch(List.of(refs.get(0),refs.get(0))));
assertThrows(ResponseStatusException.class,()->service.resolveBatch(List.of(new AppointmentCoverageService.SourceRef("OTHER","ORDER:1","FBA:ONT8"))));
}
@@ -35,6 +35,9 @@ class AppointmentWorkPersistenceTest {
assertEquals("运营甲",store.replay(state.getWorkItemKey(),"same-key").getOperatorName());
assertThrows(RuntimeException.class,()->store.history(state,new Actor("2","运营乙",ACTOR.permissions()),"SAVE_FOLLOWUP","其他人重复键","same-key","other",Map.of("version",2)));
assertEquals(1,store.history(state.getWorkItemKey()).size());
mapper.insertCommitment("FBA_APPOINTMENT:1","{}");mapper.updateCommitment("FBA_APPOINTMENT:1","{}",3);
var tokens=new AppointmentWorkCommitmentService(mapper,new ObjectMapper()).impactTokens(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2"));
assertEquals(Map.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:1:3","FBA_APPOINTMENT:2","FBA_APPOINTMENT:2:0"),tokens);
}
}
@Test void transactionRollbackCannotLeaveHalfGeneratedRoute() throws Exception {
@@ -12,6 +12,60 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
class AppointmentWorkQueryServiceTest {
@Test void followTodayPageOneSizeEightDoesNotReadUnrelatedRuntimeFactsOrPaginateBeforeCompletionChecks() {
var f=new WorkTestFixture();
for(int index=0;index<1000;index++) f.add(f.state("unrelated-"+index,Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null));
var expected=new HashSet<String>();
var prior=f.state("due-1",Type.PICKUP,Flow.TRANSFER,null);f.add(prior);expected.add(prior.getWorkItemKey());
for(int index=1;index<=10;index++) {
var state=f.state("due-"+index,Type.DELIVERY,Flow.TRANSFER,"inbound");state.setFollowupDate(LocalDate.of(2026,10,14));
if(index==1) state.setPrerequisites(List.of(prior.getWorkItemKey()));
if(index==10) f.facts.put(state.getWorkItemKey(),fact(state,null,null,ExecutionStatus.COMPLETED,new Actual(Instant.parse("2026-10-14T12:00:00Z"),null,"America/Los_Angeles")));
f.add(state);expected.add(state.getWorkItemKey());
}
var future=f.state("future",Type.DELIVERY,Flow.TRANSFER,"inbound");future.setFollowupDate(LocalDate.of(2027,1,1));f.add(future);
// Exact parameters from /items?view=FOLLOW_TODAY&page=1&size=8.
var page=f.queries.items(View.FOLLOW_TODAY,1,8,null,null,null,null,null,null,ACTOR);
assertEquals(9,page.total());assertEquals(8,page.records().size());assertEquals(2,page.pages());
assertTrue(page.records().stream().allMatch(i->i.executionStatus()==ExecutionStatus.PENDING));
verify(f.gateway).facts(argThat(states->states.stream().map(AppointmentWorkState::getWorkItemKey).collect(java.util.stream.Collectors.toSet()).equals(expected)));
}
@Test void followTodayPreservesAuthoritativeDateLineZonesAndUndatedPendingHandoffs() {
var f=new WorkTestFixture();
var tomorrow=LocalDate.of(2026,10,16);
var east=f.state("east",Type.DELIVERY,Flow.TRANSFER,"inbound");east.setFollowupDate(tomorrow);f.add(east);
var west=f.state("west",Type.DELIVERY,Flow.TRANSFER,"inbound");west.setFollowupDate(tomorrow);f.add(west);
// Stored timezone is Los Angeles; the live source is already on the following local day.
f.facts.put(east.getWorkItemKey(),fact(east,new Slot(tomorrow,null,null,null,"Pacific/Kiritimati"),null,ExecutionStatus.PENDING,null));
var handoff=f.state("return",Type.EMPTY_RETURN,Flow.TRANSFER,null);
var owner=new Responsibility(ResponsibilityKind.USER,"1","运营甲");
handoff.setReturnHandoff(new ReturnHandoff(1,owner,owner,HandoffStatus.PENDING_ACCEPTANCE,"交接"));f.add(handoff);
var query=new AppointmentWorkQueryService(f.store,f.gateway,Clock.fixed(Instant.parse("2026-10-15T12:00:00Z"),ZoneOffset.UTC));
var page=query.items(View.FOLLOW_TODAY,1,8,null,null,null,null,null,null,ACTOR);
assertEquals(Set.of(east.getWorkItemKey(),handoff.getWorkItemKey()),page.records().stream().map(WorkItem::workItemKey).collect(java.util.stream.Collectors.toSet()));
}
@Test void calendarLoadsOnlyRequestedTypesButUsesCurrentAuthorityDatesAndStillChecksScope() {
var f=new WorkTestFixture();
var receipt=f.state("1",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null);receipt.setTarget(slot("2025-01-01"));f.add(receipt);
var pickup=f.state("1",Type.PICKUP,Flow.TRANSFER,null);f.add(pickup);
receipt.setPrerequisites(List.of(pickup.getWorkItemKey()));f.store.save(receipt);
f.facts.put(receipt.getWorkItemKey(),fact(receipt,slot("2026-10-16"),null,ExecutionStatus.PENDING,null));
assertEquals(1,f.queries.calendar(LocalDate.of(2026,10,1),LocalDate.of(2026,10,31),Flow.TRANSFER,Set.of(Type.WAREHOUSE_RECEIPT),ACTOR).total());
verify(f.gateway).facts(argThat(states->states.size()==1 && states.get(0).getWorkItemKey().equals(receipt.getWorkItemKey())));
verify(f.gateway).discover(null,1);
var reader=new Actor("2","无原业务权限",Set.of(LIST));
assertEquals(0,f.queries.calendar(LocalDate.of(2026,10,1),LocalDate.of(2026,10,31),null,Set.of(Type.WAREHOUSE_RECEIPT),reader).total());
}
@Test void filteredItemsLoadPrerequisiteClosureWithoutIncludingUnrelatedContainers() {
var f=new WorkTestFixture();
var pickup=f.state("1",Type.PICKUP,Flow.TRANSFER,null);f.add(pickup);
var retired=f.state("1",Type.ROUTE,Flow.UNKNOWN,null);retired.setRetired(true);retired.setReplacementKeys(List.of(pickup.getWorkItemKey()));f.add(retired);
var receipt=f.state("1",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null);receipt.setPrerequisites(List.of(retired.getWorkItemKey()));receipt.setObservedFingerprint("old");f.add(receipt);
f.add(f.state("2",Type.DELIVERY,Flow.TRANSFER,"other"));
var items=f.queries.items(View.ALL,1,20,null,null,Set.of(Type.WAREHOUSE_RECEIPT),"1",null,null,ACTOR);
assertEquals(1,items.total());assertEquals(pickup.getWorkItemKey(),items.records().get(0).nextAction().targetWorkItemKey());
verify(f.gateway).facts(argThat(states->states.stream().map(AppointmentWorkState::getWorkItemKey).collect(java.util.stream.Collectors.toSet()).equals(Set.of(pickup.getWorkItemKey(),receipt.getWorkItemKey()))));
}
@Test void globalSummaryRetainsUnknownRoutesAndReturnAfterDeliveryAcrossCalendarFilters() {
var f=new WorkTestFixture(); var route=f.state("1",Type.ROUTE,Flow.UNKNOWN,null); route.setFollowupDate(LocalDate.of(2026,10,14)); f.add(route);
var returns=f.state("2",Type.EMPTY_RETURN,Flow.DIRECT,null); returns.setTarget(slot("2026-09-01")); f.add(returns);
@@ -87,6 +87,8 @@ class FleetTripAuthorizationTest {
}
@Configuration @EnableMethodSecurity
static class Config {
@Bean com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges workTripChanges() { return mock(com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges.class); }
@Bean com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort workDispatches() { return mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class); }
@Bean FleetTripCalendarService calendar() { return mock(FleetTripCalendarService.class); }
@Bean FleetCapacityOfferCommandMapper commands() { return mock(FleetCapacityOfferCommandMapper.class); }
@Bean FleetDispatchPlanningGateway planning() { return mock(FleetDispatchPlanningGateway.class); }
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.fleet.trip.work;
import com.baiyue.lastleg.appointmentwork.AppointmentWorkCommitmentService;
import com.baiyue.lastleg.fleet.driver.FleetWarehouseClient;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.stream.IntStream;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AppointmentWorkTransportProviderTest {
@Test void unassignedTransportWorkDoesNotQueryTripsOrWarehousesPerSource() {
var mapper=mock(AppointmentWorkTripMapper.class);var warehouses=mock(FleetWarehouseClient.class);var commitments=mock(AppointmentWorkCommitmentService.class);
var requests=IntStream.range(0,200).mapToObj(i->new FactRequest("W"+i,Type.TRANSPORT,Flow.TRANSFER,"C"+i,"INBOUND:"+i,null,null,List.of(),List.of(),List.of())).toList();
var facts=new AppointmentWorkTransportProvider(mapper,warehouses,commitments,"token").read(requests);
assertEquals(200,facts.size());assertTrue(facts.stream().allMatch(f->!f.planExists() && f.executionStatus()==ExecutionStatus.PENDING));
verifyNoInteractions(mapper,warehouses,commitments);
}
}
@@ -62,6 +62,16 @@ class InboundFbaAppointmentServiceTest {
SourceFbaAppointmentRequest request(String key,FbaAppointmentDetails details,String... sources) {return new SourceFbaAppointmentRequest(requestRefs(sources),"100","ONT8",details,key);}
FbaAppointmentResponse create(String key) {return service.createFromSources("operator",request(key,pending(null),"ORDER:1","ORDER:2"));}
@Test void workBatchSnapshotsReadOnlyHeadersAndItemsWithoutRunningRepartitionEligibility() {
var original=create("snapshot");
when(headers.selectBatchIds(List.of(original.id()))).thenReturn(List.copyOf(storedHeaders));
clearInvocations(headers,items,query,sources);
assertEquals(List.of(original),service.batchSnapshots(List.of(original.batchKey())));
verify(headers).selectBatchIds(List.of(original.id()));verify(items).selectList(any());verifyNoMoreInteractions(headers,items);verifyNoInteractions(query,sources);
assertThrows(ResponseStatusException.class,()->service.batchSnapshots(List.of("DISPATCH:1")));
assertThrows(ResponseStatusException.class,()->service.batchSnapshots(List.of("FBA_APPOINTMENT:999")));
}
@Test void sourceRequestKeepsWireShapeAndPassesAllSourceFieldsToResolution() throws Exception {
var json=new ObjectMapper().findAndRegisterModules();
var input=request("source-json",pending(null),"ORDER:1","ORDER:2");
@@ -81,7 +81,9 @@ class InboundPlanPaginationTest {
String sql = String.join("", InboundPlanQueryMapper.class
.getMethod("rows", String.class, InboundPlanRequests.Query.class, long.class, long.class)
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
assertThat(sql).contains("ai2.customer_id=p.customer_id", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("ai2.customer_id=p.customer_id", "ai2.fba_warehouse_code=l.fba_warehouse_code",
"a2.replacement_nos_json IS NULL", "ORDER BY ai2.active DESC,a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("COUNT(DISTINCT current_item.appointment_id)", "current_item.active=1", "THEN 'MULTIPLE'");
assertThat(sql).contains("p.customer_id=#{customerId}", "o.customer_id=p.customer_id");
assertThat(sql).doesNotContain("a2.customer_id=#{customerId}");
assertThat(sql).contains("di.customer_id=p.customer_id AND di.order_id=l.order_id",
@@ -1,6 +1,8 @@
package com.baiyue.lastleg.inboundplan.work;
import com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationService;
import com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService.SourceRef;
import com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.Resolution;
import com.baiyue.lastleg.appointmentwork.AppointmentWorkCommitmentService;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import org.junit.jupiter.api.Test;
@@ -12,9 +14,28 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AppointmentWorkDeliveryProviderTest {
@Test void establishedFbaBatchesShareSnapshotsDeliveriesAndImpactReads() {
var appointments=mock(InboundFbaAppointmentService.class);var cargo=mock(AppointmentCargoVerificationService.class);
var commitments=mock(AppointmentWorkCommitmentService.class);var deliveries=mock(AppointmentWorkDeliveryMapper.class);var sources=mock(AppointmentWorkSourceMapper.class);
var requests=java.util.stream.IntStream.rangeClosed(1,200).mapToObj(i->new FactRequest("W"+i,Type.DELIVERY,Flow.TRANSFER,"1",null,"FBA_APPOINTMENT:"+i,null,List.of(),List.of(),List.of())).toList();
var keys=requests.stream().map(FactRequest::batchKey).toList();
var snapshots=keys.stream().map(key->{
var snapshot=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.class);
when(snapshot.batchKey()).thenReturn(key);when(snapshot.id()).thenReturn(Long.valueOf(key.substring(16)));when(snapshot.appointmentStatus()).thenReturn("PENDING");
return snapshot;
}).toList();
when(appointments.batchSnapshots(keys)).thenReturn(snapshots);
when(commitments.impactTokens(keys)).thenReturn(keys.stream().collect(java.util.stream.Collectors.toMap(k->k,k->k+":0")));
var result=new AppointmentWorkDeliveryProvider(appointments,cargo,commitments,deliveries,sources).read(requests);
assertEquals(200,result.size());assertTrue(result.stream().allMatch(f->f.planExists() && f.executionStatus()==ExecutionStatus.PENDING));
verify(appointments).batchSnapshots(keys);verify(deliveries).batches(keys);verify(commitments).impactTokens(keys);
verifyNoMoreInteractions(appointments,deliveries,commitments);verifyNoInteractions(cargo,sources);
when(appointments.batchSnapshots(keys)).thenReturn(List.of());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->new AppointmentWorkDeliveryProvider(appointments,cargo,commitments,deliveries,sources).read(requests));
}
@Test void handledPrerequisiteCommitsTheCallerTransactionThroughTheRealCargoProxy() {
var sourceService=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class);
when(sourceService.resolve("ORDER:11","C",null)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"));
when(sourceService.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new Resolution(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"))));
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper());
var ds=new org.h2.jdbcx.JdbcDataSource();ds.setURL("jdbc:h2:mem:work-read-tx-"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
var manager=new org.springframework.jdbc.datasource.DataSourceTransactionManager(ds);
@@ -33,11 +54,11 @@ class AppointmentWorkDeliveryProviderTest {
var cargo=mock(AppointmentCargoVerificationService.class);var sources=mock(AppointmentWorkSourceMapper.class);
var provider=new AppointmentWorkDeliveryProvider(mock(InboundFbaAppointmentService.class),cargo,mock(AppointmentWorkCommitmentService.class),mock(AppointmentWorkDeliveryMapper.class),sources);
var request=new FactRequest("W",Type.DELIVERY,Flow.TRANSFER,"1","INBOUND:1",null,null,List.of(),List.of(),List.of(new SourceReference("C","ORDER:11",null)));
when(cargo.inspect("ORDER:11","C",null)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中"));
when(cargo.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new AppointmentCargoVerificationService.Inspection(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中"))));
var fact=provider.read(List.of(request)).get(0);
assertEquals(ExecutionStatus.PENDING,fact.executionStatus());assertNull(fact.action());assertNull(fact.actual());
assertEquals(List.of("SOURCE_CONFIGURATION"),fact.missingFields());assertEquals(List.of("接仓点不在启用的美国仓目录中"),fact.blockReasons());
doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"服务不可用")).when(cargo).inspect("ORDER:11","C",null);
doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"服务不可用")).when(cargo).inspectBatch(List.of(new SourceRef("C","ORDER:11",null)));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(List.of(request)));
}
@Test void privateCompletionCoversThisDispatchPerSourceWithoutCompletingTheWholeOrder() {
@@ -42,8 +42,31 @@ class AppointmentWorkDomainMapperTest {
assertEquals(0,dispatch.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",1));
var deliveries=session.getMapper(AppointmentWorkDeliveryMapper.class);assertTrue(deliveries.batch("FBA_APPOINTMENT:1").isEmpty());assertTrue(deliveries.customer("1","1").isEmpty());
assertTrue(deliveries.privateBatch("DISPATCH:1").isEmpty());
assertTrue(deliveries.batches(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2")).isEmpty());
var sources=session.getMapper(AppointmentWorkSourceMapper.class);assertTrue(sources.containerSources("1").isEmpty());assertTrue(sources.privateDispatches("1").isEmpty());assertTrue(sources.privateBatch("DISPATCH:1").isEmpty());
var receiving=session.getMapper(AppointmentWorkReceivingMapper.class);assertTrue(receiving.plans("1").isEmpty());assertNull(receiving.session("1"));
var receiving=session.getMapper(AppointmentWorkReceivingMapper.class);assertTrue(receiving.plans(List.of("1")).isEmpty());assertTrue(receiving.sessions(List.of("1")).isEmpty());
try(var sql=session.getConnection().createStatement()) {
sql.execute("INSERT INTO ll_order(id,order_no,customer_id,source_region,source_inbound_id,source_event_id,source_loading_plan_id,source_payload_hash,source_container_booking_id) VALUES(101,'O-1','A','TEST','I-1','E-1','P-1','HASH','1'),(102,'O-2','B','TEST','I-2','E-2','P-2','HASH','2')");
sql.execute("INSERT INTO ll_inbound_plan(id,plan_no,customer_id,source_order_id,container_no,planned_arrival_date) VALUES(201,'IP-1','A',101,'C-1','2026-10-16'),(202,'IP-2','B',102,'C-2','2026-10-17')");
sql.execute("INSERT INTO ll_inbound_plan_line(id,customer_id,plan_id,order_id,inbound_code) VALUES(301,'A',201,101,'IN-1'),(302,'B',202,102,'IN-2'),(303,'WRONG',201,102,'WRONG')");
sql.execute("INSERT INTO ll_receiving_container_session(id,source_container_booking_id,container_fact_hash,container_no,overseas_warehouse_id,overseas_warehouse_code,overseas_warehouse_name,operator_key,status,created_at) VALUES(401,'1','HASH','C-1',1,'W','W','A','COMPLETED','2026-10-15 01:00:00'),(402,'1','HASH','C-1',1,'W','W','A','RECEIVING','2026-10-15 02:00:00'),(403,'1','HASH','C-1',1,'W','W','A','SIGNED','2026-10-15 02:00:00'),(404,'2','HASH','C-2',1,'W','W','B','WAIT_SIGN','2026-10-15 01:00:00')");
}
session.clearCache();
var plans=receiving.plans(List.of("1","2"));assertEquals(2,plans.size());
assertEquals("1",plans.get(0).getSourceContainerBookingId());assertEquals("IP-1",plans.get(0).getPlanNo());assertEquals(java.time.LocalDate.of(2026,10,16),plans.get(0).getPlannedArrivalDate());
assertEquals("2",plans.get(1).getSourceContainerBookingId());
var sourceBatch=sources.containerSourcesBatch(List.of("1","2"));assertEquals(2,sourceBatch.size());
assertEquals("1",sourceBatch.get(0).containerBookingId());assertEquals(sources.containerSources("1").get(0),sourceBatch.get(0).source());
var sessions=receiving.sessions(List.of("1","2"));assertEquals(2,sessions.size());
assertEquals(403L,sessions.stream().filter(s->"1".equals(s.getSourceContainerBookingId())).findFirst().orElseThrow().getId());
try(var sql=session.getConnection().createStatement()) {
sql.execute("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(501,'A','D-1','PENDING_DEPARTURE'),(502,'B','D-2','CANCELLED'),(503,'B','D-3','PENDING_DEPARTURE')");
sql.execute("INSERT INTO ll_dispatch_appointment_allocation(dispatch_id,dispatch_item_id,plan_pallet_id,batch_key,customer_id,source_key,destination_key,package_count) VALUES(501,601,701,'FBA_APPOINTMENT:1','A','ORDER:101','FBA:ONT8',4),(501,602,702,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',6),(502,603,703,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',2),(503,604,704,'FBA_APPOINTMENT:2','B','ORDER:102','FBA:ONT8',3)");
}
session.clearCache();
var batchDeliveries=deliveries.batches(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2"));assertEquals(3,batchDeliveries.size());
assertEquals(deliveries.batch("FBA_APPOINTMENT:1"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:1")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList());
assertEquals(deliveries.batch("FBA_APPOINTMENT:2"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:2")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList());
var trips=session.getMapper(AppointmentWorkTripMapper.class);assertTrue(trips.batchTrips("FBA_APPOINTMENT:1").isEmpty());assertTrue(trips.batches(1).isEmpty());assertNull(trips.trip(1));
}
}
@@ -13,9 +13,10 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
class AppointmentWorkReceivingProviderTest {
@Test void receivingIsInProgressWithoutInventingAnActualCompletionAndTargetCarriesRealIdentity() {
var mapper=mock(AppointmentWorkReceivingMapper.class); var provider=new AppointmentWorkReceivingProvider(mapper);
var plan=new InboundPlan(); plan.setId(10L);plan.setPlanNo("IP-REAL");plan.setCustomerId("C");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
var plan=new AppointmentWorkReceivingMapper.ContainerPlan(); plan.setSourceContainerBookingId("1");plan.setId(10L);plan.setPlanNo("IP-REAL");plan.setCustomerId("C");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
var session=new ReceivingContainerSession();session.setId(2L);session.setStatus("RECEIVING");session.setReceivingAt(LocalDateTime.of(2026,10,15,10,0));
when(mapper.plans("1")).thenReturn(List.of(plan)); when(mapper.session("1")).thenReturn(session);
session.setSourceContainerBookingId("1");
when(mapper.plans(List.of("1"))).thenReturn(List.of(plan)); when(mapper.sessions(List.of("1"))).thenReturn(List.of(session));
var request=new FactRequest("work",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,"1",null,null,null,List.of(),List.of(),List.of());
var fact=provider.read(List.of(request)).get(0);
assertEquals(ExecutionStatus.IN_PROGRESS,fact.executionStatus()); assertNull(fact.actual());
@@ -23,4 +24,14 @@ class AppointmentWorkReceivingProviderTest {
session.setStatus("COMPLETED"); session.setCompletedAt(LocalDateTime.of(2026,10,16,12,0));
fact=provider.read(List.of(request)).get(0); assertEquals(ExecutionStatus.COMPLETED,fact.executionStatus()); assertEquals(session.getCompletedAt(),fact.actual().localOccurredAt());
}
@Test void twoQueriesCoverTwoHundredContainersAndKeepTheirFactsSeparate() {
var mapper=mock(AppointmentWorkReceivingMapper.class); var provider=new AppointmentWorkReceivingProvider(mapper);
var requests=java.util.stream.IntStream.range(1,201).mapToObj(i->new FactRequest("W"+i,Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,""+i,null,null,null,List.of(),List.of(),List.of())).toList();
var plan=new AppointmentWorkReceivingMapper.ContainerPlan();plan.setSourceContainerBookingId("2");plan.setId(20L);plan.setPlanNo("IP-2");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
when(mapper.plans(org.mockito.ArgumentMatchers.anyList())).thenReturn(List.of(plan));
var facts=provider.read(requests);assertEquals(200,facts.size());assertNull(facts.get(0).target());assertEquals(plan.getPlannedArrivalDate(),facts.get(1).target().date());
verify(mapper).plans(requests.stream().map(FactRequest::containerBookingId).toList());verify(mapper).sessions(requests.stream().map(FactRequest::containerBookingId).toList());verifyNoMoreInteractions(mapper);
when(mapper.plans(org.mockito.ArgumentMatchers.anyList())).thenReturn(java.util.Collections.nCopies(1001,plan));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(requests));
}
}