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

85 lines
5.2 KiB
Java

package com.baiyue.lastleg.appointmentplanning;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/** Stage-specific planning rules. Planning never creates stock, vehicles or carrier confirmations. */
public final class AppointmentPlanningPolicy {
private AppointmentPlanningPolicy() { }
public record Cargo(String sourceKey, String customerId, String originWarehouseId,
String destinationKey, String originCountry, String destinationCountry,
Integer packages, Instant readyFrom, Instant readyTo,
boolean eligibleForPlanning, String blockReason) { }
public record Group(String originWarehouseId, String destinationKey, Long packages,
int sourceCount, int customerCount, Instant readyFrom, Instant readyTo,
boolean readinessIncomplete, int unknownQuantitySources) { }
public record Forecast(String sourceKey, String sourceVersion, Instant eta, Instant readyAt) { }
public record Impact(String sourceKey, String reason, Instant previousEta, Instant latestEta,
Instant confirmedAt, boolean reviewRequired) { }
public static Group group(List<Cargo> cargo) {
if (cargo == null || cargo.isEmpty()) throw new IllegalArgumentException("请选择需要安排的真实货物");
if (cargo.size() > 500) throw new IllegalArgumentException("每批最多选择500条来源货物");
Set<String> sources = new HashSet<>();
Set<String> customers = new HashSet<>();
String origin = null, destination = null;
long packages = 0;
int unknownQuantitySources = 0;
Instant from = null, to = null;
boolean incomplete = false;
for (Cargo row : cargo) {
if (row == null || blank(row.sourceKey()) || blank(row.customerId())) {
throw new IllegalArgumentException("货物缺少稳定来源或客户归属");
}
if (!sources.add(row.sourceKey())) throw new IllegalArgumentException("同一来源货物不能重复选择");
if (!row.eligibleForPlanning()) throw new IllegalArgumentException(
blank(row.blockReason()) ? "所选货物暂不具备筹划资格" : row.blockReason());
if (!"US".equals(row.originCountry()) || !"US".equals(row.destinationCountry())) {
throw new IllegalArgumentException("请选择已确认美国起运点及目的地的货物");
}
if (blank(row.originWarehouseId()) || blank(row.destinationKey())) {
throw new IllegalArgumentException("货物缺少实际接仓点或目的地");
}
if (origin == null) { origin = row.originWarehouseId(); destination = row.destinationKey(); }
if (!origin.equals(row.originWarehouseId()) || !destination.equals(row.destinationKey())) {
throw new IllegalArgumentException("同批货物必须具有相同实际起运仓和目的地");
}
if (row.packages() == null || row.packages() <= 0) unknownQuantitySources++;
else packages = Math.addExact(packages, row.packages().longValue());
customers.add(row.customerId());
if (row.readyFrom() == null || row.readyTo() == null) incomplete = true;
if (row.readyFrom() != null && row.readyTo() != null && row.readyTo().isBefore(row.readyFrom())) {
throw new IllegalArgumentException("货物预计备妥窗口无效");
}
if (row.readyFrom() != null && (from == null || row.readyFrom().isAfter(from))) from = row.readyFrom();
if (row.readyTo() != null && (to == null || row.readyTo().isBefore(to))) to = row.readyTo();
}
if (from != null && to != null && to.isBefore(from)) throw new IllegalArgumentException("所选货物预计备妥窗口不相容,请分别安排");
return new Group(origin, destination, unknownQuantitySources==0?packages:null, sources.size(), customers.size(), from, to, incomplete, unknownQuantitySources);
}
/** A new forecast only requests review; it never changes an externally confirmed appointment. */
public static Impact compare(Forecast acknowledged, Forecast latest, Instant confirmedAt) {
if (acknowledged == null || latest == null || blank(latest.sourceKey())
|| !Objects.equals(acknowledged.sourceKey(), latest.sourceKey())) {
throw new IllegalArgumentException("只能比较同一真实货源的预测");
}
boolean changed = !Objects.equals(acknowledged.eta(), latest.eta())
|| !Objects.equals(acknowledged.readyAt(), latest.readyAt());
String reason = "UNCHANGED";
if (changed) {
reason = latest.eta() == null ? "FORECAST_MISSING" : "FORECAST_CHANGED";
if (confirmedAt != null && latest.readyAt() != null && latest.readyAt().isAfter(confirmedAt)) {
reason = "READINESS_AFTER_APPOINTMENT";
}
}
return new Impact(latest.sourceKey(), reason, acknowledged.eta(), latest.eta(), confirmedAt, changed);
}
private static boolean blank(String value) { return value == null || value.isBlank(); }
}