package jnpf.dmsBusinessOccupancy;
|
|
import org.mybatis.spring.SqlSessionTemplate;
|
import java.util.*;
|
import static jnpf.dmsBusinessOccupancy.BusinessOccupancyException.Reason.*;
|
|
/** Trusted business execution only; caller owns the transaction and completion evidence. */
|
public final class BusinessFileExecutionService {
|
private final BusinessOccupancyTransactions transactions;
|
private final BusinessFileOccupancyCoordinator coordinator;
|
private final BusinessFileEligibilityService eligibility;
|
private final SqlSessionTemplate sessions;
|
public BusinessFileExecutionService(SqlSessionTemplate sessions) {
|
this.sessions=sessions;
|
transactions=new BusinessOccupancyTransactions(sessions);
|
coordinator=new BusinessFileOccupancyCoordinator(sessions);
|
eligibility=new BusinessFileEligibilityService(sessions);
|
}
|
/** Locks the original numbers before document locks and verifies the frozen source versions. */
|
public List<BusinessFileOccupancy> require(String tenant,String chain,String application,String workflowTask,String operation,
|
Collection<String> fileIds) {
|
return transactions.execute(mapper->{
|
Set<String> files=new HashSet<>(fileIds);
|
if(files.isEmpty() || files.size()!=fileIds.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
var all=mapper.chainOccupancies(tenant,chain);
|
var receipts=all.stream().filter(r->files.contains(r.getFileId())).toList();
|
if(receipts.size()!=files.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
if(!coordinator.tryLockNumbers(tenant,receipts.stream().map(BusinessFileOccupancy::getFileNumber).toList()))
|
throw new BusinessOccupancyException(NUMBER_BUSY);
|
var current=mapper.fileTargets(tenant,new ArrayList<>(files));
|
if(current.size()!=files.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
List<BusinessFileOccupancy> result=new ArrayList<>();
|
for(var old:receipts) {
|
var row=mapper.chain(tenant,old.getFileNumber(),chain);
|
if(row==null || row.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED);
|
if(!Objects.equals(row.getApplicationId(),application) || !Objects.equals(row.getWorkflowTaskId(),workflowTask)
|
|| !Objects.equals(row.getOperation(),operation)) throw new BusinessOccupancyException(TARGET_CHANGED);
|
var target=current.stream().filter(t->Objects.equals(t.getFileId(),row.getFileId())).findFirst()
|
.orElseThrow(()->new BusinessOccupancyException(TARGET_CHANGED));
|
if(!Objects.equals(target.getFileNumber(),row.getFileNumber())
|
|| !Objects.equals(target.getBusinessVersionId(),row.getBusinessVersionId())
|
|| !Objects.equals(target.getCenterVersionId(),row.getCenterVersionId()))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
result.add(row);
|
}
|
return List.copyOf(result);
|
});
|
}
|
/** Extension executes the whole application; subset execution is not a valid completion. */
|
public List<BusinessFileOccupancy> requireExtension(String tenant,String application,String task,
|
Collection<String> fileIds) {
|
return transactions.execute(mapper->{
|
var frozen=mapper.chainOccupancies(tenant,task).stream()
|
.filter(row->Objects.equals(application,row.getApplicationId())
|
&& Objects.equals(task,row.getWorkflowTaskId())
|
&& "EXTENSION".equals(row.getOperation())).toList();
|
if(frozen.isEmpty()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
if(!coordinator.tryLockNumbers(tenant,frozen.stream().map(BusinessFileOccupancy::getFileNumber).toList()))
|
throw new BusinessOccupancyException(NUMBER_BUSY);
|
var requested=new HashSet<>(fileIds);
|
var locked=mapper.chainOccupancies(tenant,task).stream()
|
.filter(row->Objects.equals(application,row.getApplicationId())
|
&& Objects.equals(task,row.getWorkflowTaskId())
|
&& "EXTENSION".equals(row.getOperation())).toList();
|
if(requested.isEmpty() || requested.size()!=fileIds.size() || locked.size()!=requested.size()
|
|| !requested.equals(new HashSet<>(locked.stream().map(BusinessFileOccupancy::getFileId).toList()))
|
|| !new HashSet<>(frozen.stream().map(BusinessFileOccupancy::getId).toList())
|
.equals(new HashSet<>(locked.stream().map(BusinessFileOccupancy::getId).toList())))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
return require(tenant,task,application,task,"EXTENSION",fileIds);
|
});
|
}
|
public List<BusinessFileOccupancy> requireRevision(String tenant,String auditId) {
|
return new BusinessRevisionSubmissionService(sessions).require(tenant,auditId);
|
}
|
public List<BusinessFileOccupancy> requireCreation(String tenant,String auditId) {
|
return transactions.execute(mapper->{
|
var before=mapper.creationNumbers(tenant,auditId);
|
var rows=requireCreation(tenant,auditId,before);
|
var after=mapper.creationNumbers(tenant,auditId);
|
if(before.size()!=after.size() || !new HashSet<>(before).equals(new HashSet<>(after)))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
return rows;
|
});
|
}
|
/** Reserve an external approval's own number; it has no issued-number detail row. */
|
public List<BusinessFileOccupancy> requireExternalCreation(String tenant, String auditId,
|
String taskId, String fileNumber) {
|
return transactions.execute(mapper -> {
|
if (!coordinator.tryLockNumbers(tenant, List.of(fileNumber)))
|
throw new BusinessOccupancyException(NUMBER_BUSY);
|
BusinessFileOccupancy previous = mapper.chain(tenant, fileNumber, taskId);
|
if (previous == null && mapper.hasBusinessNumber(tenant, fileNumber))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
BusinessFileOccupancy requested = new BusinessFileOccupancy();
|
requested.setTenantId(tenant); requested.setFileNumber(fileNumber);
|
requested.setOperation("CREATE"); requested.setChainId(taskId);
|
requested.setApplicationId(auditId); requested.setWorkflowTaskId(taskId);
|
requested.setStage("EXECUTING");
|
BusinessFileOccupancy receipt = coordinator.acquire(requested);
|
return List.of(receipt);
|
});
|
}
|
/** Every CREATE source item must be covered; called before activation and again before projection. */
|
public List<BusinessFileOccupancy> requireCreation(String tenant,String auditId,Collection<String> numbers) {
|
return transactions.execute(mapper->{
|
var requested=new HashSet<>(numbers);
|
if(requested.isEmpty() || requested.size()!=numbers.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
if(!coordinator.tryLockNumbers(tenant,requested)) throw new BusinessOccupancyException(NUMBER_BUSY);
|
var rows=mapper.creationReceipts(tenant,auditId);
|
if(rows.size()!=requested.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
for(var row:rows) {
|
if(row.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED);
|
if(!requested.contains(row.getFileNumber())) throw new BusinessOccupancyException(TARGET_CHANGED);
|
}
|
return rows;
|
});
|
}
|
public void guardNumbers(String tenant,String chain,Collection<String> numbers) {
|
if(numbers.isEmpty()) return;
|
transactions.execute(mapper->{
|
if(!coordinator.tryLockNumbers(tenant,numbers)) throw new BusinessOccupancyException(NUMBER_BUSY);
|
for(String number:numbers) {
|
var row=mapper.active(tenant,number);
|
if(row!=null && !Objects.equals(chain,row.getChainId())) throw new BusinessOccupancyException(ALREADY_OCCUPIED);
|
}
|
return null;
|
});
|
}
|
/** Standalone center files retain their path; resource-backed files require the full audit batch. */
|
public void guardStandaloneActivation(String tenant,String centerVersionId) {
|
transactions.execute(mapper->{
|
String number=mapper.centerFileNumber(tenant,centerVersionId);
|
if(number!=null && !number.isBlank()) guardNumbers(tenant,null,List.of(number));
|
if(!Objects.equals(number,mapper.centerFileNumber(tenant,centerVersionId))
|
|| mapper.hasBusinessDocument(tenant,centerVersionId))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
return null;
|
});
|
}
|
/** Generic initial upload must not claim a number already owned by a business resource/chain. */
|
public void guardStandaloneRegistration(String tenant,String fileNumber) {
|
if(fileNumber==null || fileNumber.isBlank()) return;
|
transactions.execute(mapper->{
|
guardNumbers(tenant,null,List.of(fileNumber));
|
if(mapper.hasBusinessNumber(tenant,fileNumber)) throw new BusinessOccupancyException(TARGET_CHANGED);
|
return null;
|
});
|
}
|
public void guardValidity(String tenant,String task,Collection<String> versions) {
|
transactions.execute(mapper->{
|
List<BusinessFileTarget> targets=new ArrayList<>();
|
for(String version:versions) {
|
var found=mapper.expiryTargets(tenant,version);
|
if(found.isEmpty() && !mapper.hasCurrentBusinessReference(tenant,version)) continue;
|
if(found.size()!=1) throw new BusinessOccupancyException(TARGET_CHANGED);
|
targets.add(found.get(0));
|
}
|
if(targets.isEmpty()) return null;
|
guardNumbers(tenant,task,targets.stream().map(BusinessFileTarget::getFileNumber).toList());
|
if(task!=null) {
|
var all=mapper.chainOccupancies(tenant,task);
|
for(var target:targets) {
|
var row=all.stream().filter(r->Objects.equals(r.getFileId(),target.getFileId())).findFirst()
|
.orElseThrow(()->new BusinessOccupancyException(TARGET_CHANGED));
|
require(tenant,task,row.getApplicationId(),task,"EXTENSION",List.of(target.getFileId()));
|
if(!Objects.equals(target.getCenterVersionId(),row.getCenterVersionId()))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
}
|
} else {
|
var checks=eligibility.recheckForSubmission(targets.stream().map(t->
|
new BusinessFileEligibilityService.Query(tenant,t.getFileNumber(),t.getFileId(),t.getBusinessVersionId(),
|
BusinessFileEligibilityService.Operation.REVIEW_EXTENSION)).toList());
|
if(checks.stream().anyMatch(d->!d.allowed())) throw new BusinessOccupancyException(TARGET_CHANGED);
|
}
|
return null;
|
});
|
}
|
/** Cascading records must also respect number occupancy, even when not explicit application rows. */
|
public void guardVersions(String tenant,String chain,Collection<String> versions) {
|
transactions.execute(mapper->{
|
List<BusinessFileTarget> targets=new ArrayList<>();
|
for(String version:versions) {
|
var found=mapper.expiryTargets(tenant,version);
|
if(found.size()!=1) throw new BusinessOccupancyException(TARGET_CHANGED);
|
targets.add(found.get(0));
|
}
|
if(targets.isEmpty()) throw new BusinessOccupancyException(TARGET_CHANGED);
|
if(!coordinator.tryLockNumbers(tenant,targets.stream().map(BusinessFileTarget::getFileNumber).toList()))
|
throw new BusinessOccupancyException(NUMBER_BUSY);
|
for(var target:targets) {
|
if(!List.of(target).equals(mapper.expiryTargets(tenant,target.getCenterVersionId())))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
var occupied=mapper.active(tenant,target.getFileNumber());
|
if(occupied!=null && !Objects.equals(chain,occupied.getChainId()))
|
throw new BusinessOccupancyException(ALREADY_OCCUPIED);
|
}
|
return null;
|
});
|
}
|
public List<BusinessFileOccupancy> terminationCandidates(String afterId,int limit) {
|
return transactions.reader().terminationCandidates(afterId,limit);
|
}
|
/** Re-read authoritative terminal evidence under number and workflow locks; no timeout release. */
|
public boolean releaseEnded(BusinessFileOccupancy candidate) {
|
return transactions.execute(mapper->{
|
if(!coordinator.tryLockNumbers(candidate.getTenantId(),List.of(candidate.getFileNumber()))) return false;
|
if(mapper.lockEndedTask(candidate)==null) return false;
|
return coordinator.release(candidate.getTenantId(),candidate.getFileNumber(),candidate.getChainId(),candidate.getId(),"ENDED_UNSUCCESSFUL");
|
});
|
}
|
/** Call only after all native detail/resource/version projections succeeded in this transaction. */
|
public void complete(List<BusinessFileOccupancy> receipts) {
|
for(var row:receipts) if(!coordinator.release(row.getTenantId(),row.getFileNumber(),row.getChainId(),row.getId(),"EXECUTED"))
|
throw new BusinessOccupancyException(TARGET_CHANGED);
|
}
|
}
|