Al Huda ERP State Machines
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.