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 require(String tenant,String chain,String application,String workflowTask,String operation, Collection fileIds) { return transactions.execute(mapper->{ Set 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 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 requireExtension(String tenant,String application,String task, Collection 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 requireRevision(String tenant,String auditId) { return new BusinessRevisionSubmissionService(sessions).require(tenant,auditId); } public List 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 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 requireCreation(String tenant,String auditId,Collection 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 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 versions) { transactions.execute(mapper->{ List 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 versions) { transactions.execute(mapper->{ List 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 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 receipts) { for(var row:receipts) if(!coordinator.release(row.getTenantId(),row.getFileNumber(),row.getChainId(),row.getId(),"EXECUTED")) throw new BusinessOccupancyException(TARGET_CHANGED); } }