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 appointments, List 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 query(List 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.of() : external.resolve(externalRefs); var bookingMap=bookings.stream().collect(Collectors.toMap(s -> key(s.customerId(),"BOOKING:"+s.bookingNo()),s -> s)); var refs=new LinkedHashMap(); 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.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(); 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(); var seen=new HashSet(); 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 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"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(); } }