Files
edr-platform/apps/edr-freight-web/backoffice/src/components/ruleEngine/rateMatrixRules.ts
2026-06-02 10:47:36 +03:00

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();