fix(appointment): exclude cancelled batches without active allocations

This commit is contained in:
2026-10-16 16:21:51 +08:00
parent 000125c3ec
commit 58685945b1
3 changed files with 62 additions and 8 deletions
@@ -20,6 +20,7 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa
public AppointmentWorkRequirementProvider(AppointmentSourceService sources,AppointmentCargoVerificationService cargo,InboundFbaAppointmentService appointments,AppointmentWorkSourceMapper sourceMapping) {
this.sources=sources; this.cargo=cargo; this.appointments=appointments; this.sourceMapping=sourceMapping;
}
/** 按当前实物分配生成客户需求,保留已撤销批次身份以退役旧事项。 */
public List<Requirement> requirements(ContainerSource container,Flow effectiveFlow) {
var original=container.requirements().stream().map(r->"PRIVATE".equals(r.destinationKind())?new Requirement(r.requirementKey(),r.batchKey(),r.batchNo(),"CUSTOMER",r.originName(),r.destinationName(),r.timezone(),r.references(),r.replacesRequirementKeys(),r.sources()):r).toList();
if(effectiveFlow==Flow.DIRECT) return original;
@@ -53,9 +54,14 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa
// Unknown remaining quantity is still work, never a zero or an absent requirement.
boolean remainder=resolved.stream().map(cargo::inspect).anyMatch(c->c.availablePackages()==null || c.availablePackages()>0);
var replaced=history.stream().filter(a->!active(a)).map(FbaAppointmentResponse::batchKey).toList();
if(remainder) result.put(requirement.requirementKey(),new Requirement(requirement.requirementKey(),null,null,requirement.destinationKind(),
requirement.originName(),requirement.destinationName(),requirement.timezone(),requirement.references(),replaced,
complete.stream().map(r->new SourceReference(r.customerId(),r.sourceKey(),r.destinationKey())).toList()));
if(remainder) {
var restored=new LinkedHashSet<>(replaced);
// 回到未分配需求时,同时承接原批次运输事项和按客户拆分的送仓事项。
complete.forEach(ref->replaced.forEach(key->restored.add(customerRequirement(key,ref.customerId()))));
result.put(requirement.requirementKey(),new Requirement(requirement.requirementKey(),null,null,requirement.destinationKind(),
requirement.originName(),requirement.destinationName(),requirement.timezone(),requirement.references(),List.copyOf(restored),
complete.stream().map(r->new SourceReference(r.customerId(),r.sourceKey(),r.destinationKey())).toList()));
}
for(var batch:active) {
var replacementKeys=new LinkedHashSet<>(replaced); replacementKeys.add(requirement.requirementKey());
history.stream().filter(a->a.replacementBatchKeys()!=null && a.replacementBatchKeys().contains(batch.batchKey())).map(FbaAppointmentResponse::batchKey).forEach(replacementKeys::add);
@@ -118,9 +124,11 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa
if(result.size()!=input.size()) throw unavailable("来源键必须明确为ORDER或BOOKING");
return result.stream().distinct().toList();
}
/** 已取消且没有当前实物分配的记录只属历史;取消但仍有货物的批次继续产生需求。 */
private static boolean active(FbaAppointmentResponse batch) {
// Cancelling a booking clears its schedule, not its physical allocation. Only an
// authoritative replacement retires the batch and its delivery/vehicle obligations.
return batch.replacementBatchKeys()==null || batch.replacementBatchKeys().isEmpty();
if(batch.replacementBatchKeys()!=null && !batch.replacementBatchKeys().isEmpty()) return false;
// 客户来源取当前活动分配;空取消记录不能制造无货送仓事项,也不能阻断整份清单。
// 剩余货量仍由 cargo.inspect 核算,有货的 CANCELLED/FAILED 批次不因预约状态退役。
return !("CANCELLED".equals(batch.appointmentStatus()) && batch.items().isEmpty());
}
}