// ruleEngine/rateMatrixRules.ts import { RATE_TYPES, REQUIRED_RATE_TYPES, MATRIX_STATUS } from '@/constants/rateMatrixConstants'; interface RateEntry { rateType: string; entries: Array>; } interface ValidationRule { id: string; description: string; severity: 'error' | 'warning'; validate: (data: any) => boolean; message: string; } export class RateMatrixRulesEngine { private rules: ValidationRule[] = []; constructor() { this.initializeRules(); } private initializeRules() { // Rule 1: All rate types must be present this.rules.push({ id: 'ALL_TYPES_REQUIRED', description: 'Verify all 13 rate types are included', severity: 'error', validate: (rateSections: RateEntry[]) => { const submittedTypes = rateSections.map(s => s.rateType); return REQUIRED_RATE_TYPES.every(type => submittedTypes.includes(type)); }, message: 'All 13 rate types must be included in the submission', }); // Rule 2: Each rate type must have at least one entry this.rules.push({ id: 'MINIMUM_ENTRIES', description: 'Each rate type requires at least one rate entry', severity: 'error', validate: (rateSections: RateEntry[]) => { return rateSections.every(section => section.entries.length > 0); }, message: 'Each rate type must have at least one rate entry', }); // Rule 3: Dates must be valid this.rules.push({ id: 'VALID_DATES', description: 'Rate entries must have valid date ranges', severity: 'error', validate: (rateSections: RateEntry[]) => { return rateSections.every(section => section.entries.every(entry => { if (!entry.validFrom) return false; if (entry.validTo && new Date(entry.validTo) <= new Date(entry.validFrom)) { return false; } return true; }) ); }, message: 'All rate entries must have valid dates (Valid To must be after Valid From)', }); // Rule 4: Rates must be non-negative this.rules.push({ id: 'NON_NEGATIVE_RATES', description: 'All rate values must be non-negative', severity: 'error', validate: (rateSections: RateEntry[]) => { const numericFields = ['baseRate', 'ratePerMetricTon', 'ratePerKm', 'ratePerTrip', 'ratePerDay', 'ratePerUnit']; return rateSections.every(section => section.entries.every(entry => { return numericFields.every(field => { const value = entry[field]; return value === undefined || value === '' || Number(value) >= 0; }); }) ); }, message: 'Rate values cannot be negative', }); // Rule 5: Business rule - Demurrage free days should be reasonable this.rules.push({ id: 'DEMURRAGE_FREE_DAYS', description: 'Demurrage free days should be between 0 and 30', severity: 'warning', validate: (rateSections: RateEntry[]) => { const demurrageSection = rateSections.find( s => s.rateType === RATE_TYPES.DEMURRAGE ); if (!demurrageSection) return true; return demurrageSection.entries.every(entry => { const freeDays = Number(entry.freeDays); return !freeDays || (freeDays >= 0 && freeDays <= 30); }); }, message: 'Demurrage free days typically range from 0 to 30 days', }); // Rule 6: Cancellation fee percentage should be 0-100 this.rules.push({ id: 'CANCELLATION_FEE_RANGE', description: 'Cancellation fee percentage must be between 0 and 100', severity: 'error', validate: (rateSections: RateEntry[]) => { const cancellationSection = rateSections.find( s => s.rateType === RATE_TYPES.CANCELLATION_FEE ); if (!cancellationSection) return true; return cancellationSection.entries.every(entry => { const percentage = Number(entry.cancellationFeePercentage); return !percentage || (percentage >= 0 && percentage <= 100); }); }, message: 'Cancellation fee percentage must be between 0 and 100', }); } validate(data: RateEntry[]) { const errors: Array<{ ruleId: string; message: string; severity: string }> = []; const warnings: Array<{ ruleId: string; message: string; severity: string }> = []; this.rules.forEach(rule => { if (!rule.validate(data)) { const issue = { ruleId: rule.id, message: rule.message, severity: rule.severity, }; if (rule.severity === 'error') { errors.push(issue); } else { warnings.push(issue); } } }); return { isValid: errors.length === 0, errors, warnings, }; } // Check if matrix can transition to a new status canTransition(fromStatus: string, toStatus: string, userRole: string): boolean { const transitions: Record> = { [MATRIX_STATUS.DRAFT]: [ { to: MATRIX_STATUS.PENDING_APPROVAL, allowedRoles: ['Director'] }, ], [MATRIX_STATUS.PENDING_APPROVAL]: [ { to: MATRIX_STATUS.ACTIVE, allowedRoles: ['Chief Executive'] }, { to: MATRIX_STATUS.REJECTED, allowedRoles: ['Chief Executive'] }, ], }; const allowedTransitions = transitions[fromStatus] || []; const transition = allowedTransitions.find(t => t.to === toStatus); return transition ? transition.allowedRoles.includes(userRole) : false; } } export const rateMatrixRulesEngine = new RateMatrixRulesEngine();