Files
baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageService.java
T

110 lines
8.5 KiB
Java

package com.baiyue.lastleg.appointmentplanning;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/** Shared read projection. Callers authorize the customer/source before invoking; never confirms bookings. */
@Service
public class AppointmentCoverageService {
public record SourceRef(String customerId, String sourceKey, String destinationKey) { }
public record Appointment(String appointmentNo, String confirmationNo, String status,
LocalDateTime confirmedAt, LocalDateTime confirmedEndAt, String timezone) { }
public record Coverage(String customerId, String sourceKey, String destinationKey, String status,
List<Appointment> appointments, List<String> missingFields,
String sourceVersion, boolean changeReviewRequired) { }
private final AppointmentCoverageMapper mapper;
private final AppointmentExternalSourceGateway external;
public AppointmentCoverageService(AppointmentCoverageMapper mapper, AppointmentExternalSourceGateway external) {
this.mapper=mapper; this.external=external;
}
public List<Coverage> query(List<SourceRef> input) {
if (input==null || input.isEmpty()) return List.of();
if (input.size()>100 || input.stream().anyMatch(s -> s==null || blank(s.customerId()) || blank(s.sourceKey()) || blank(s.destinationKey()))) {
throw new IllegalArgumentException("每次最多核对100个完整的预约来源");
}
var requested=input.stream().distinct().toList();
var externalRefs=requested.stream().filter(s -> s.sourceKey().startsWith("BOOKING:"))
.map(s -> new AppointmentExternalSourceGateway.SourceRef(s.customerId(),s.sourceKey().substring(8))).toList();
var bookings=externalRefs.isEmpty() ? List.<AppointmentExternalSourceGateway.Source>of() : external.resolve(externalRefs);
var bookingMap=bookings.stream().collect(Collectors.toMap(s -> key(s.customerId(),"BOOKING:"+s.bookingNo()),s -> s));
var refs=new LinkedHashMap<SourceRef,AppointmentCoverageMapper.OrderRef>();
for (var source:requested) {
if (source.sourceKey().startsWith("ORDER:")) {
try {
long id=Long.parseLong(source.sourceKey().substring(6));
if (id>0) refs.put(source,new AppointmentCoverageMapper.OrderRef(source.customerId(),id));
} catch (NumberFormatException ignored) { /* Invalid source is returned explicitly below. */ }
} else if (source.sourceKey().startsWith("BOOKING:")) {
var booking=bookingMap.get(key(source.customerId(),source.sourceKey()));
if (booking!=null && booking.orderId()!=null && Objects.equals(source.destinationKey(),booking.destinationKey())) {
refs.put(source,new AppointmentCoverageMapper.OrderRef(source.customerId(),booking.orderId()));
}
}
}
var unique=refs.values().stream().distinct().toList();
if (unique.size()!=refs.size()) throw new IllegalArgumentException("同一货物存在重复来源,请保留原外接登记来源后再核对");
var orders=unique.isEmpty() ? List.<AppointmentCoverageMapper.Source>of() : mapper.sources(unique);
var orderMap=orders.stream().collect(Collectors.toMap(s -> new AppointmentCoverageMapper.OrderRef(s.customerId(),s.orderId()),s -> s));
var facts=mapper.appointments(requested);
var bySource=facts.stream().collect(Collectors.groupingBy(f -> key(f.customerId(),f.sourceKey())));
var output=new ArrayList<Coverage>();
for (var source:requested) {
var booking=bookingMap.get(key(source.customerId(),source.sourceKey()));
if (booking!=null && !source.destinationKey().equals(booking.destinationKey())) {
output.add(empty(source,"DESTINATION_MISMATCH","DESTINATION",version(booking.recordVersion()),true)); continue;
}
if (booking!=null && !Set.of("CONFIRMED","RECEIVED","BOUND").contains(booking.bookingStatus()==null ? "" : booking.bookingStatus())) {
output.add(empty(source,"SOURCE_NOT_READY","CARRIER_CONFIRMATION",version(booking.recordVersion()),false)); continue;
}
var order=orderMap.get(refs.get(source));
var relevant=bySource.getOrDefault(key(source.customerId(),source.sourceKey()),List.of());
if (order==null) {
if(booking!=null && !relevant.isEmpty()) {
var planned=relevant.stream().map(f->new Appointment(f.appointmentNo(),null,f.status(),null,null,f.timezone())).distinct().toList();
output.add(new Coverage(source.customerId(),source.sourceKey(),source.destinationKey(),"PENDING",planned,List.of("REAL_CARGO_MATERIALS","RECEIVING_CONFIRMATION"),version(booking.recordVersion()),false));continue;
}
output.add(empty(source,booking==null ? "SOURCE_NOT_FOUND" : "SOURCE_NOT_MATERIALIZED","REAL_CARGO_MATERIALS",booking==null ? null : version(booking.recordVersion()),false)); continue;
}
if(blank(order.destinationType()) || blank(order.fbaWarehouseCode())) {
output.add(empty(source,"SOURCE_NOT_READY","DESTINATION_UNVERIFIED",version(order.version()),false));continue;
}
if (!"FBA".equals(order.destinationType()) || !source.destinationKey().equals("FBA:"+order.fbaWarehouseCode())) {
output.add(empty(source,"DESTINATION_MISMATCH","DESTINATION",version(order.version()),true)); continue;
}
var distinct=new LinkedHashMap<String,Appointment>();
var seen=new HashSet<Long>(); long confirmed=0, covered=0; boolean invalid=false, changed=false;
for (var fact:relevant) {
if (fact.orderId()!=null && !Objects.equals(order.orderId(),fact.orderId()) || !Objects.equals(order.fbaWarehouseCode(),fact.warehouseCode()) || fact.packages()==null || fact.packages()<=0 || !seen.add(fact.itemId())) { invalid=true; continue; }
covered+=fact.packages();
boolean success="SUCCESS".equals(fact.status()) && fact.appointmentAt()!=null && !blank(fact.confirmationNo()) && !blank(fact.timezone());
if (success) confirmed+=fact.packages();
distinct.putIfAbsent(fact.appointmentNo(),new Appointment(fact.appointmentNo(),success ? fact.confirmationNo() : null,fact.status(),success ? fact.appointmentAt() : null,success ? fact.appointmentEndAt() : null,fact.timezone()));
String currentVersion=(booking==null?"":"BOOKING:"+version(booking.recordVersion())+"/")+"ORDER:"+version(order.version());
changed|=!Objects.equals(currentVersion,fact.sourceVersion());
}
var missing=new ArrayList<String>();
String status;
if (invalid || order.packages()==null || order.packages()<=0 || covered>order.packages()) { status="REVIEW_REQUIRED"; missing.add("CARGO_QUANTITY_REVIEW"); }
else if (covered==0) { status="UNASSIGNED"; missing.add("RECEIVING_APPOINTMENT"); }
else if (covered<order.packages()) { status="PARTIAL"; missing.add("UNCOVERED_CARGO"); }
else if (confirmed<order.packages()) { status="PENDING"; missing.add("RECEIVING_CONFIRMATION"); }
else { status="CONFIRMED"; }
if (relevant.stream().anyMatch(f->"SUCCESS".equals(f.status()) && blank(f.timezone()))) missing.add("APPOINTMENT_TIMEZONE_UNVERIFIED");
if (order.shipmentReferencesMissing()) missing.add("FBA_SHIPMENT_REFERENCES");
if (changed) missing.add("SOURCE_CHANGED_REVIEW");
output.add(new Coverage(source.customerId(),source.sourceKey(),source.destinationKey(),status,List.copyOf(distinct.values()),List.copyOf(missing),
(booking==null ? "" : "BOOKING:"+version(booking.recordVersion())+"/")+"ORDER:"+version(order.version()),changed || invalid));
}
return List.copyOf(output);
}
private static Coverage empty(SourceRef s,String status,String missing,String version,boolean review) {
return new Coverage(s.customerId(),s.sourceKey(),s.destinationKey(),status,List.of(),List.of(missing),version,review);
}
private static String key(String customerId,String sourceKey) { return customerId+"\n"+sourceKey; }
private static String version(Number version) { return version==null ? null : version.toString(); }
private static boolean blank(String value) { return value==null || value.isBlank(); }
}