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

activateFromSanctioning

When a sanctioning record is APPROVED, this method generates a constrained tournamentRecord and transitions the sanctioning to ACTIVE.

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';