Sanctioning Engine
import { sanctioningEngine } from 'tods-competition-factory';
The sanctioningEngine is a standalone state engine that manages the lifecycle of tournament sanctioning applications. It operates on SanctioningRecord documents — propositional objects that describe tournaments an organizer intends to run, subject to approval by a governing body. Once approved, a sanctioning record seeds the creation of a constrained tournamentRecord.
The engine follows the same patterns as the sync/async engines (state management, executionQueue with piping and rollback) but manages its own state independently from tournament records.
State Machine
The sanctioning engine implements a multi-step workflow with the following statuses:
Terminal states: REJECTED, WITHDRAWN, CLOSED — no further transitions allowed.
State Management
reset
Clears all sanctioning records from engine state.
sanctioningEngine.reset();
getState / setState
// Get all records (deep copy)
const { sanctioningRecords } = sanctioningEngine.getState();
// Load records into engine
sanctioningEngine.setState(sanctioningRecords);
setActiveSanctioningId / getActiveSanctioningId
When multiple records are loaded, set the active record for method calls that don't specify a sanctioningId.
sanctioningEngine.setActiveSanctioningId('sanc-001');
const activeId = sanctioningEngine.getActiveSanctioningId();
Creating Records
createSanctioningRecord
Creates a new sanctioning record in DRAFT status. Automatically assigns UUIDs to the record and each event proposal.
{
sanctioningId?: string; // optional — auto-generated if omitted
governingBodyId: string; // required — which body sanctions this
applicant: Applicant; // required — who is applying
proposal: TournamentProposal; // required — what tournament is proposed
sanctioningLevel?: string; // e.g., "W50", "Level 3"
sanctioningPolicy?: string; // policy name to validate against
}
Returns: { success, sanctioningRecord }
const result = sanctioningEngine.createSanctioningRecord({
governingBodyId: 'usta',
applicant: {
organisationName: 'Cary Tennis Center',
contactName: 'Jane Doe',
contactEmail: 'jane@carytennis.com',
},
proposal: {
tournamentName: 'Cary Open 2027',
proposedStartDate: '2027-06-01',
proposedEndDate: '2027-06-07',
events: [
{ eventName: "Men's Singles", eventType: 'SINGLES', gender: 'MALE', drawSize: 32 },
{ eventName: "Women's Singles", eventType: 'SINGLES', gender: 'FEMALE', drawSize: 32 },
],
},
sanctioningLevel: 'Level 3',
});
Proposal Editing
Proposals can only be edited when the record is in DRAFT or MODIFICATION_REQUESTED status.
updateProposal
Updates top-level proposal fields. Does not modify the events array (use event proposal methods for that).
sanctioningEngine.updateProposal({
updates: { tournamentName: 'Cary Open Championship', surfaceCategory: 'HARD' },
});
addEventProposal / removeEventProposal / updateEventProposal
// Add
const { eventProposalId } = sanctioningEngine.addEventProposal({
eventProposal: { eventName: 'Mixed Doubles', eventType: 'DOUBLES', gender: 'MIXED' },
});
// Update
sanctioningEngine.updateEventProposal({
eventProposalId,
updates: { drawSize: 16, matchUpFormat: 'SET3-S:6/TB7' },
});
// Remove
sanctioningEngine.removeEventProposal({ eventProposalId });
Endorsement
Some governing bodies require a national federation endorsement before submission. The endorsement is an inline sub-workflow on the sanctioning record.
// Request endorsement from national federation
sanctioningEngine.requestEndorsement({
endorserId: 'usta-section-5',
endorserName: 'USTA Southern Section',
});
// Federation endorses
sanctioningEngine.endorseApplication({
endorserNotes: 'Facilities verified',
conditions: ['Must use approved tournament software'],
});
// Or declines
sanctioningEngine.declineEndorsement({
declineReason: 'Venue does not meet minimum court requirements',
});
Workflow Transitions
submitApplication
Transitions from DRAFT to SUBMITTED. If a sanctioningPolicy is provided with requireEndorsement: true, submission is blocked unless the endorsement status is ENDORSED or NOT_REQUIRED.
The policy is snapshot'd onto the record at submission time — all subsequent validation uses the snapshot.
sanctioningEngine.submitApplication({
sanctioningPolicy: POLICY_SANCTIONING_USTA,
submittedBy: 'jane@carytennis.com',
});
reviewApplication / approveApplication / rejectApplication
// Begin review
sanctioningEngine.reviewApplication({
reviewer: { reviewerId: 'rev-001', reviewerName: 'John Reviewer' },
});
// Approve
sanctioningEngine.approveApplication({ approvedBy: 'John Reviewer' });
// Or reject
sanctioningEngine.rejectApplication({
rejectedBy: 'John Reviewer',
reason: 'Insufficient facilities for requested level',
});
conditionallyApprove / meetCondition
sanctioningEngine.conditionallyApprove({
conditions: [{ description: 'Submit insurance certificate' }, { description: 'Confirm medical plan' }],
});
// Later, meet each condition
const record = sanctioningEngine.getSanctioningRecord().sanctioningRecord;
for (const condition of record.conditions) {
const { allConditionsMet } = sanctioningEngine.meetCondition({
conditionId: condition.conditionId,
});
if (allConditionsMet) {
sanctioningEngine.approveApplication({}); // now fully approved
}
}
requestModification / withdrawApplication
sanctioningEngine.requestModification({
requestedBy: 'Reviewer',
note: 'Please increase draw size to 64',
});
// Applicant modifies and resubmits
sanctioningEngine.updateProposal({ updates: { ... } });
sanctioningEngine.submitApplication({ sanctioningPolicy });
Tournament Generation
openProposalRegistration
Opens public registration on a proposal before it is activated into a tournamentRecord. This lets people register against a proposal that has not yet become a tournament — for example a public site rendering a registration page during the approval window.
It assigns a tournamentId to the proposal (minting one if none is supplied), gives each proposed event a stable eventId, merges any supplied registrationProfile fields, and opens entriesOpen when no explicit value is present. activateFromSanctioning later reuses these same ids, so registrations collected now survive activation.
Registration can be (re)opened from any non-terminal status; only REJECTED, WITHDRAWN, and CLOSED are rejected. Stricter workflow gating (e.g. requiring approval first) is left to the consuming service.
const { tournamentId, registrationProfile } = sanctioningEngine.openProposalRegistration({
registrationProfile: { entriesOpen: '2026-05-01' },
});
// tournamentId is now stable on the proposal and reused at activation
activateFromSanctioning
When a sanctioning record is APPROVED, this method generates a constrained tournamentRecord and transitions the sanctioning to ACTIVE. It reuses any tournamentId and per-event eventIds already assigned by openProposalRegistration so pre-activation registrations remain valid; otherwise new ids are minted.
The generated tournament carries:
- Events with
allowedDrawTypesfrom event proposals processCodes: ['SANCTIONED']parentOrganisationIdfrom the governing body- Sanctioning ID stored as an extension
- A compliance checklist generated from the policy's
postEventRequirements
const { tournamentRecord } = sanctioningEngine.activateFromSanctioning({
sanctioningPolicy: POLICY_SANCTIONING_ITF,
});
// tournamentRecord is ready to load into tournamentEngine
tournamentEngine.setState(tournamentRecord);
Amendments
After approval, modifications follow an amendment sub-workflow rather than full re-submission.
// Propose an amendment
const { severity, autoApproved } = sanctioningEngine.proposeAmendment({
changes: [{ field: 'tournamentName', previousValue: 'Old Name', proposedValue: 'New Name', changeType: 'MODIFIED' }],
sanctioningPolicy,
});
// Minor changes are auto-approved; substantial changes need review
// Review a substantial amendment
sanctioningEngine.reviewAmendment({
amendmentId: '...',
approved: true,
reviewerNotes: 'Date change approved',
});
Severity classification is policy-driven via amendmentRules.substantialChangeFields. Fields matching those patterns (including wildcards like events.*.drawSize) are classified as SUBSTANTIAL.
Post-Event Compliance
After a tournament completes, the sanctioning record tracks compliance items.
// Transition to post-event
sanctioningEngine.transitionToPostEvent({});
// Submit compliance items
sanctioningEngine.submitComplianceItem({
itemId: '...',
value: { fileUrl: 'https://results.example.com' },
});
// Verify items
const { allCompliant } = sanctioningEngine.verifyComplianceItem({ itemId: '...' });
// Close when compliant
if (allCompliant) {
sanctioningEngine.closeApplication({});
}
Queries
getSanctioningRecord
Returns a deep copy of the active sanctioning record.
getAvailableTransitions
Returns valid status transitions for the current record.
getStatusHistory
Returns the full status transition history.
getCompleteness
Returns a completeness score (0-100%) with missing fields. Optionally accepts a sanctioningPolicy to include policy-driven field requirements.
const { completeness } = sanctioningEngine.getCompleteness({ sanctioningPolicy });
// { score: 85, totalFields: 14, completedFields: 12, missingFields: [...] }
getEligibleTiers
Returns which policy tiers the current proposal is eligible for, with reasons for ineligibility.
const { eligibleTiers, tierEligibilities } = sanctioningEngine.getEligibleTiers({
sanctioningPolicy: POLICY_SANCTIONING_USTA,
});
validateProposal
Validates the proposal against a sanctioning policy and optional tier. Returns structured issues with severity (error or warning).
const { valid, errors, warnings, issues } = sanctioningEngine.validateProposal({
sanctioningPolicy,
sanctioningTier: 'Level 3',
});
getCalendarConflicts
Detects scheduling conflicts with existing sanctioned events. Requires injected calendarContext (the engine doesn't fetch data).
const { conflicts, hasConflicts } = sanctioningEngine.getCalendarConflicts({
calendarContext: {
existingEvents: [...], // provided by server
calendarRules: { proximityWeeks: 2, maxEventsPerWeek: 5 },
},
});
executionQueue
Batch execution with piping and rollback, identical to the tournament engine pattern.
const result = sanctioningEngine.executionQueue([
{ method: 'createSanctioningRecord', params: { ... } },
{ method: 'submitApplication', params: { sanctioningPolicy } },
{ method: 'reviewApplication', params: {} },
{ method: 'approveApplication', params: {} },
{ method: 'activateFromSanctioning', params: { sanctioningPolicy } },
], true); // rollbackOnError
Policy Fixtures
Three sanctioning policies are included as fixtures:
| Policy | Governing Body | Tiers |
|---|---|---|
POLICY_SANCTIONING_GENERIC | Generic | Level 1, 2, 3 |
POLICY_SANCTIONING_ITF | ITF | M15/W15, M25/W25, W35, W50, W75, W100 |
POLICY_SANCTIONING_USTA | USTA | Level 7 (local) through Level 1 (national) |
Each policy defines: tier constraints (draw types, sizes, formats, prize money, courts, officials), personnel rules, calendar rules, amendment rules, and post-event compliance requirements.
import { POLICY_SANCTIONING_ITF } from 'tods-competition-factory/fixtures/policies/POLICY_SANCTIONING_ITF';