mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
173 lines
5.6 KiB
TypeScript
173 lines
5.6 KiB
TypeScript
// ruleEngine/rateMatrixRules.ts
|
|
import { RATE_TYPES, REQUIRED_RATE_TYPES, MATRIX_STATUS } from '@/constants/rateMatrixConstants';
|
|
|
|
interface RateEntry {
|
|
rateType: string;
|
|
entries: Array<Record<string, any>>;
|
|
}
|
|
|
|
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<string, Array<{ to: string; allowedRoles: string[] }>> = {
|
|
[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(); |