Skip to main content

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 allowedDrawTypes from event proposals
  • processCodes: ['SANCTIONED']
  • parentOrganisationId from 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:

PolicyGoverning BodyTiers
POLICY_SANCTIONING_GENERICGenericLevel 1, 2, 3
POLICY_SANCTIONING_ITFITFM15/W15, M25/W25, W35, W50, W75, W100
POLICY_SANCTIONING_USTAUSTALevel 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';