package jnpf.dmsBusinessOccupancy;
|
|
import org.mybatis.spring.SqlSessionTemplate;
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.Objects;
|
import java.util.HashSet;
|
|
/**
|
* Shared admission conditions for NEW competing lifecycle applications against an existing file.
|
* No native form CRUD, no HTTP controller and no automatic runtime registration.
|
* First creation, trusted chain continuation, expiry, errata and usage tasks use their own contracts.
|
*/
|
public final class BusinessFileEligibilityService {
|
public enum Operation { REVISION, REVOCATION, INVALIDATION, EXTENSION, REVIEW_EXTENSION }
|
public enum Reason {
|
FILE_NOT_FOUND, AMBIGUOUS_FILE_NUMBER, FILE_NOT_EFFECTIVE, VERSION_CHANGED,
|
INCONSISTENT_CURRENT_VERSION, PENDING_VERSION, REVOCATION_IN_PROGRESS_OR_SUCCEEDED,
|
REVOCATION_STATE_UNKNOWN, FILE_OCCUPIED, NUMBER_BUSY
|
}
|
/** tenantId comes from authenticated server context; IDs are resource/business-version IDs. */
|
public record Query(String tenantId, String fileNumber, String fileId,
|
String businessVersionId, Operation operation) {
|
public Query {
|
valid(tenantId, 50); valid(fileNumber, 200); valid(fileId, 50); valid(businessVersionId, 50);
|
Objects.requireNonNull(operation, "operation");
|
}
|
private static void valid(String value, int max) {
|
if (value == null || value.isBlank() || !value.equals(value.strip()) || value.indexOf('\0') >= 0
|
|| value.codePointCount(0, value.length()) > max)
|
throw new IllegalArgumentException("Invalid eligibility query identity");
|
}
|
}
|
/** Machine codes only; business display text remains online-configurable. */
|
public record Decision(List<Reason> reasons, String fileId, String businessVersionId, String centerVersionId) {
|
public Decision { reasons = List.copyOf(reasons); }
|
public boolean allowed() { return reasons.isEmpty(); }
|
}
|
private final BusinessOccupancyTransactions transactions;
|
private final BusinessFileOccupancyCoordinator coordinator;
|
public BusinessFileEligibilityService(SqlSessionTemplate sessions) {
|
transactions = new BusinessOccupancyTransactions(sessions);
|
coordinator = new BusinessFileOccupancyCoordinator(sessions);
|
}
|
/** Read-only hint. It neither acquires occupancy nor promises a later submission will succeed. */
|
public Decision query(Query query) {
|
Objects.requireNonNull(query);
|
return evaluate(query, transactions.reader().eligibilityFacts(query.tenantId(), query.fileNumber()));
|
}
|
/** Single-file form of the batch recheck. No occupancy or application is written here. */
|
public Decision recheckForSubmission(Query query) {
|
return recheckForSubmission(List.of(Objects.requireNonNull(query))).get(0);
|
}
|
/**
|
* Lock the entire number set in the coordinator's stable order, then reread all facts in the
|
* caller's verified transaction, BEFORE document/resource locks. Caller must require ALL
|
* decisions allowed, acquire occupancy and persist natively in this same transaction.
|
* No own-chain exclusion is accepted from the caller. Continuation is a separate trusted path.
|
*/
|
public List<Decision> recheckForSubmission(List<Query> queries) {
|
List<Query> batch = List.copyOf(queries);
|
if (batch.isEmpty()) throw new IllegalArgumentException("Empty eligibility batch");
|
String tenantId = batch.get(0).tenantId();
|
var numbers = new HashSet<String>();
|
for (Query query : batch) {
|
if (!tenantId.equals(query.tenantId()) || !numbers.add(query.fileNumber()))
|
throw new IllegalArgumentException("Batch must contain unique numbers in one tenant");
|
}
|
if (!coordinator.tryLockNumbers(tenantId, numbers))
|
return batch.stream().map(q -> new Decision(List.of(Reason.NUMBER_BUSY), null, null, null)).toList();
|
return transactions.execute(mapper -> batch.stream().map(query -> evaluate(query,
|
mapper.eligibilityFacts(query.tenantId(), query.fileNumber()))).toList());
|
}
|
static Decision evaluate(Query query, BusinessFileEligibilityFacts facts) {
|
Objects.requireNonNull(facts, "Missing database facts");
|
List<Reason> reasons = new ArrayList<>();
|
if (facts.getFileCount() == 0) reasons.add(Reason.FILE_NOT_FOUND);
|
else if (facts.getFileCount() != 1) reasons.add(Reason.AMBIGUOUS_FILE_NUMBER);
|
else {
|
if (!facts.isEffective()) reasons.add(Reason.FILE_NOT_EFFECTIVE);
|
if (!Objects.equals(query.fileId(), facts.getFileId())
|
|| !Objects.equals(query.businessVersionId(), facts.getBusinessVersionId()))
|
reasons.add(Reason.VERSION_CHANGED);
|
if (facts.getEffectiveVersionCount() != 1) reasons.add(Reason.INCONSISTENT_CURRENT_VERSION);
|
if (facts.isPendingVersion()) reasons.add(Reason.PENDING_VERSION);
|
if (query.operation() == Operation.REVISION) {
|
if (facts.isRevocationBlocking()) reasons.add(Reason.REVOCATION_IN_PROGRESS_OR_SUCCEEDED);
|
if (facts.isRevocationUnknown()) reasons.add(Reason.REVOCATION_STATE_UNKNOWN);
|
}
|
}
|
if (facts.isOccupied()) reasons.add(Reason.FILE_OCCUPIED);
|
// Do not choose a winner or expose an arbitrary target when a number is ambiguous.
|
return new Decision(reasons, facts.getFileCount() == 1 ? facts.getFileId() : null,
|
facts.getFileCount() == 1 ? facts.getBusinessVersionId() : null,
|
facts.getFileCount() == 1 ? facts.getCenterVersionId() : null);
|
}
|
}
|