export interface LifecycleDefinition { statuses: { initial: string[]; active: string[]; inactive: string[]; }; transitions: Record; transition_rights?: Record; // "from→to" → rightName } export interface ValidationResult { valid: boolean; error?: string; requiredRight?: string; // Named right required for this transition, if any } const FALLBACK_RIGHT = 'ticket.modify'; export class LifecycleValidator { validateTransition( lifecycleDef: LifecycleDefinition, fromStatus: string, toStatus: string, ): ValidationResult { const allStatuses = [ ...lifecycleDef.statuses.initial, ...lifecycleDef.statuses.active, ...lifecycleDef.statuses.inactive, ]; if (!allStatuses.includes(toStatus)) { return { valid: false, error: `Status "${toStatus}" is not defined in the lifecycle`, }; } // Check for allowed transitions const allowedTransitions = this.getAllowedTransitions(lifecycleDef, fromStatus); if (allowedTransitions.includes(toStatus)) { const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus); return { valid: true, requiredRight: right ?? FALLBACK_RIGHT }; } // Also handle wildcard "*" -> any transition const wildcardTransitions = this.getAllowedTransitions(lifecycleDef, '*'); if (wildcardTransitions.includes(toStatus)) { const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus); return { valid: true, requiredRight: right ?? FALLBACK_RIGHT }; } return { valid: false, error: `Transition from "${fromStatus}" to "${toStatus}" is not allowed`, }; } /** * Get the required right for a transition using RT's 4-level priority: * 1. exact "from→to" * 2. wildcard from "*→to" * 3. wildcard to "from→*" * 4. full wildcard "*→*" * 5. fallback: ticket.modify */ getRequiredRight( lifecycleDef: LifecycleDefinition, fromStatus: string, toStatus: string, ): string | null { const rights = lifecycleDef.transition_rights ?? {}; // Priority 1: exact match if (rights[`${fromStatus}→${toStatus}`]) return rights[`${fromStatus}→${toStatus}`]; // Priority 2: wildcard from if (rights[`*→${toStatus}`]) return rights[`*→${toStatus}`]; // Priority 3: wildcard to if (rights[`${fromStatus}→*`]) return rights[`${fromStatus}→*`]; // Priority 4: full wildcard if (rights['*→*']) return rights['*→*']; // Priority 5: fallback return null; } isResolvedStatus(lifecycleDef: LifecycleDefinition, status: string): boolean { return lifecycleDef.statuses.inactive.includes(status); } private getAllowedTransitions( lifecycleDef: LifecycleDefinition, fromStatus: string, ): string[] { if (lifecycleDef.transitions[fromStatus]) { return lifecycleDef.transitions[fromStatus]!; } if (lifecycleDef.transitions['*']) { return lifecycleDef.transitions['*']!; } return []; } }