Skip to content

Alhuda ERP State Machines

Superseded — kept for history only

This page was written in January 2026 and is not a description of how the system works today. It is kept so the reasoning behind early decisions stays readable. Current behaviour is described in Customer lifecycle (LC), Visa (VISA) and Bookings. Do not build from this page.

Booking State Machine

States: - Draft - PendingOpsReview - PendingFinanceApproval - Approved - OnHold - Cancelled

Rules: - Ops approval requires finance policy satisfied.

TypeScript:

export enum BookingState {
  Draft = 'DRAFT',
  PendingOpsReview = 'PENDING_OPS_REVIEW',
  PendingFinanceApproval = 'PENDING_FINANCE_APPROVAL',
  Approved = 'APPROVED',
  OnHold = 'ON_HOLD',
  Cancelled = 'CANCELLED'
}

export function canTransitionBooking(from: BookingState, to: BookingState): boolean {
  const allowed: Record<BookingState, BookingState[]> = {
    [BookingState.Draft]: [BookingState.PendingOpsReview, BookingState.Cancelled],
    [BookingState.PendingOpsReview]: [BookingState.PendingFinanceApproval, BookingState.OnHold, BookingState.Cancelled],
    [BookingState.PendingFinanceApproval]: [BookingState.Approved, BookingState.OnHold, BookingState.Cancelled],
    [BookingState.Approved]: [BookingState.OnHold, BookingState.Cancelled],
    [BookingState.OnHold]: [BookingState.PendingOpsReview, BookingState.Cancelled],
    [BookingState.Cancelled]: []
  };
  return allowed[from].includes(to);
}

Visa State Machine

States: - NotStarted - Applied - UnderProcess - Issued - Rejected - SentToEmbassy

TypeScript:

export enum VisaState {
  NotStarted = 'NOT_STARTED',
  Applied = 'APPLIED',
  UnderProcess = 'UNDER_PROCESS',
  Issued = 'ISSUED',
  Rejected = 'REJECTED',
  SentToEmbassy = 'SENT_TO_EMBASSY'
}

export function canTransitionVisa(from: VisaState, to: VisaState): boolean {
  const allowed: Record<VisaState, VisaState[]> = {
    [VisaState.NotStarted]: [VisaState.Applied],
    [VisaState.Applied]: [VisaState.UnderProcess, VisaState.SentToEmbassy, VisaState.Rejected],
    [VisaState.UnderProcess]: [VisaState.Issued, VisaState.Rejected],
    [VisaState.Issued]: [],
    [VisaState.Rejected]: [],
    [VisaState.SentToEmbassy]: [VisaState.UnderProcess]
  };
  return allowed[from].includes(to);
}

Ticket State Machine

States: - NotReady - NameUpdatePending - OnHold - FinanceCleared - Issued

Rules: - Issued only if finance cleared OR GM exception proof exists. - Name update must be done within 2 days of departure.

TypeScript:

export enum TicketState {
  NotReady = 'NOT_READY',
  NameUpdatePending = 'NAME_UPDATE_PENDING',
  OnHold = 'ON_HOLD',
  FinanceCleared = 'FINANCE_CLEARED',
  Issued = 'ISSUED'
}

export function canTransitionTicket(from: TicketState, to: TicketState): boolean {
  const allowed: Record<TicketState, TicketState[]> = {
    [TicketState.NotReady]: [TicketState.NameUpdatePending],
    [TicketState.NameUpdatePending]: [TicketState.OnHold, TicketState.FinanceCleared],
    [TicketState.OnHold]: [TicketState.FinanceCleared],
    [TicketState.FinanceCleared]: [TicketState.Issued],
    [TicketState.Issued]: []
  };
  return allowed[from].includes(to);
}

Server-Side Validators

export function requireFinancePolicyMet(paid: number, total: number, policy: 'advance' | 'partial' | 'full') {
  if (policy === 'full' && paid < total) throw new Error('Policy requires full payment');
  if (policy === 'partial' && paid <= 0) throw new Error('Policy requires partial payment');
}

export function requireTicketClearance(isCleared: boolean, exceptionProof?: string) {
  if (!isCleared && !exceptionProof) throw new Error('Ticket issuance requires clearance or GM exception proof');
}

Example Unit Tests

import { BookingState, canTransitionBooking } from './states';

test('cannot approve booking from Draft', () => {
  expect(canTransitionBooking(BookingState.Draft, BookingState.Approved)).toBe(false);
});

test('can move Draft -> PendingOpsReview', () => {
  expect(canTransitionBooking(BookingState.Draft, BookingState.PendingOpsReview)).toBe(true);
});

Audit Log Entries

  • On each transition, write: actorId, entity, fromState, toState, reason.