fix(appointment): retain unbookable cargo as blocked work
This commit is contained in:
+8
-3
@@ -63,11 +63,16 @@ public class AppointmentWorkDeliveryProvider implements AppointmentWorkFactGatew
|
|||||||
if(request.sources().isEmpty()) { blocks.add("尾程货源尚未建立,请先核对整柜来源与入仓计划"); missing.add("LASTLEG_SOURCE"); }
|
if(request.sources().isEmpty()) { blocks.add("尾程货源尚未建立,请先核对整柜来源与入仓计划"); missing.add("LASTLEG_SOURCE"); }
|
||||||
else {
|
else {
|
||||||
for(var source:request.sources()) {
|
for(var source:request.sources()) {
|
||||||
var context=cargo.get(source.sourceKey(),source.customerId(),source.destinationKey());
|
try {
|
||||||
fingerprints.add(context.toString()); blocks.addAll(context.blockReasons());
|
var context=cargo.get(source.sourceKey(),source.customerId(),source.destinationKey());
|
||||||
|
fingerprints.add(context.toString()); blocks.addAll(context.blockReasons());
|
||||||
|
} catch(org.springframework.web.server.ResponseStatusException error) {
|
||||||
|
if(!AppointmentWorkSourceAvailability.unresolved(error)) throw error;
|
||||||
|
fingerprints.add(source+":"+error.getReason());blocks.add(error.getReason());missing.add("SOURCE_CONFIGURATION");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var first=request.sources().get(0);
|
var first=request.sources().get(0);
|
||||||
action=new Action(ActionCode.VERIFY_CARGO,"核实货物并建立预约",request.workItemKey(),new CargoTarget(first));
|
if(missing.isEmpty()) action=new Action(ActionCode.VERIFY_CARGO,"核实货物并建立预约",request.workItemKey(),new CargoTarget(first));
|
||||||
}
|
}
|
||||||
return new Fact(request.workItemKey(),digest(request.requirementKey()+":"+fingerprints),"UNBATCHED",false,PlanStatus.UNPLANNED,ExecutionStatus.PENDING,
|
return new Fact(request.workItemKey(),digest(request.requirementKey()+":"+fingerprints),"UNBATCHED",false,PlanStatus.UNPLANNED,ExecutionStatus.PENDING,
|
||||||
null,null,null,null,null,null,List.of(ORDER_READ),action==null?List.of():List.of(OverseasPermissions.INBOUND_ORDER_FBA_APPOINTMENT),action,
|
null,null,null,null,null,null,List.of(ORDER_READ),action==null?List.of():List.of(OverseasPermissions.INBOUND_ORDER_FBA_APPOINTMENT),action,
|
||||||
|
|||||||
+11
-1
@@ -36,7 +36,17 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa
|
|||||||
if("CUSTOMER".equals(requirement.destinationKind())) { customer(requirement,sourceOrders,privateDispatches,result);continue; }
|
if("CUSTOMER".equals(requirement.destinationKind())) { customer(requirement,sourceOrders,privateDispatches,result);continue; }
|
||||||
if(!"FBA".equals(requirement.destinationKind())) { result.put(requirement.requirementKey(),requirement); continue; }
|
if(!"FBA".equals(requirement.destinationKind())) { result.put(requirement.requirementKey(),requirement); continue; }
|
||||||
var refs=canonical(sourceOrders.stream().map(s->new SourceReference(s.customerId(),"ORDER:"+s.id(),null)).toList());
|
var refs=canonical(sourceOrders.stream().map(s->new SourceReference(s.customerId(),"ORDER:"+s.id(),null)).toList());
|
||||||
var resolved=sources.resolveBatch(refs);
|
List<AppointmentSourceService.Source> resolved;
|
||||||
|
try { resolved=sources.resolveBatch(refs); }
|
||||||
|
catch(org.springframework.web.server.ResponseStatusException error) {
|
||||||
|
if(!AppointmentWorkSourceAvailability.unresolved(error)) throw error;
|
||||||
|
// Keep the complete physical obligation and its canonical source identities. The fact
|
||||||
|
// provider reports the original prerequisite and exposes no booking action.
|
||||||
|
result.put(requirement.requirementKey(),new Requirement(requirement.requirementKey(),null,null,
|
||||||
|
requirement.destinationKind(),requirement.originName(),requirement.destinationName(),requirement.timezone(),
|
||||||
|
requirement.references(),List.of(),refs.stream().map(r->new SourceReference(r.customerId(),r.sourceKey(),r.destinationKey())).toList()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var complete=resolved.stream().map(s->new AppointmentCoverageService.SourceRef(s.customerId(),s.sourceKey(),s.destinationKey())).toList();
|
var complete=resolved.stream().map(s->new AppointmentCoverageService.SourceRef(s.customerId(),s.sourceKey(),s.destinationKey())).toList();
|
||||||
var history=appointments.relatedBatchSnapshots(complete);
|
var history=appointments.relatedBatchSnapshots(complete);
|
||||||
var active=history.stream().filter(AppointmentWorkRequirementProvider::active).toList();
|
var active=history.stream().filter(AppointmentWorkRequirementProvider::active).toList();
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.baiyue.lastleg.inboundplan.work;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/** An unbookable physical source remains visible work; transport and integrity errors still fail the scan. */
|
||||||
|
final class AppointmentWorkSourceAvailability {
|
||||||
|
private AppointmentWorkSourceAvailability() { }
|
||||||
|
private static final Set<String> PREREQUISITES=Set.of(
|
||||||
|
"当前货源类型不支持FBA预约", "货物尚无已开船或后续实际船节点", "当前货源不是FBA送仓货物",
|
||||||
|
"接仓点不在启用的美国仓目录中", "目的仓不在启用的美国FBA目录中", "目的地已变化,请返回原业务重新核对");
|
||||||
|
static boolean unresolved(ResponseStatusException error) {
|
||||||
|
return error.getStatusCode().value()==400 && error.getReason()!=null && PREREQUISITES.contains(error.getReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -12,6 +12,17 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
class AppointmentWorkDeliveryProviderTest {
|
class AppointmentWorkDeliveryProviderTest {
|
||||||
|
@Test void knownSourcePrerequisiteIsVisibleAndCannotOfferBookingButOutagesStillFail() {
|
||||||
|
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.get("ORDER:11","C",null)).thenThrow(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).get("ORDER:11","C",null);
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(List.of(request)));
|
||||||
|
}
|
||||||
@Test void privateCompletionCoversThisDispatchPerSourceWithoutCompletingTheWholeOrder() {
|
@Test void privateCompletionCoversThisDispatchPerSourceWithoutCompletingTheWholeOrder() {
|
||||||
var appointments=mock(InboundFbaAppointmentService.class);var deliveries=mock(AppointmentWorkDeliveryMapper.class);var sources=mock(AppointmentWorkSourceMapper.class);
|
var appointments=mock(InboundFbaAppointmentService.class);var deliveries=mock(AppointmentWorkDeliveryMapper.class);var sources=mock(AppointmentWorkSourceMapper.class);
|
||||||
var provider=new AppointmentWorkDeliveryProvider(appointments,mock(AppointmentCargoVerificationService.class),mock(AppointmentWorkCommitmentService.class),deliveries,sources);
|
var provider=new AppointmentWorkDeliveryProvider(appointments,mock(AppointmentCargoVerificationService.class),mock(AppointmentWorkCommitmentService.class),deliveries,sources);
|
||||||
|
|||||||
+17
@@ -11,6 +11,23 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
|
|||||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
||||||
|
|
||||||
class AppointmentWorkRequirementProviderTest {
|
class AppointmentWorkRequirementProviderTest {
|
||||||
|
@Test void unbookableSourceKeepsItsPhysicalRequirementWhileAuthorityFailuresStillAbort() {
|
||||||
|
var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);
|
||||||
|
var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class);
|
||||||
|
var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping);
|
||||||
|
var requirement=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of(),List.of());
|
||||||
|
var container=new ContainerSource("1","CNT",null,null,LocalDateTime.parse("2026-10-15T11:00:00"),"1",Flow.TRANSFER,null,List.of(requirement));
|
||||||
|
when(mapping.containerSources("1")).thenReturn(List.of(new AppointmentWorkSourceMapper.Source(11L,"C","10","1","FBA","ONT8",4)));
|
||||||
|
var refs=List.of(new AppointmentCoverageService.SourceRef("C","ORDER:11",null));
|
||||||
|
when(sources.canonicalOrders(anyList())).thenReturn(refs);
|
||||||
|
when(sources.resolveBatch(refs)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"));
|
||||||
|
var actual=provider.requirements(container,Flow.TRANSFER);
|
||||||
|
assertEquals(1,actual.size());assertEquals("INBOUND:1",actual.get(0).requirementKey());
|
||||||
|
assertEquals(List.of(new SourceReference("C","ORDER:11",null)),actual.get(0).sources());
|
||||||
|
verifyNoInteractions(cargo,appointments);
|
||||||
|
doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"仓库目录暂不可用,请稍后重新核对")).when(sources).resolveBatch(refs);
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.requirements(container,Flow.TRANSFER));
|
||||||
|
}
|
||||||
@Test void privateDispatchKeepsPartialRemainderAndRestoresCancelledQuantity() {
|
@Test void privateDispatchKeepsPartialRemainderAndRestoresCancelledQuantity() {
|
||||||
var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class);
|
var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class);
|
||||||
var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping);
|
var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping);
|
||||||
|
|||||||
Reference in New Issue
Block a user