import { ValidationWarning } from "./types"; import { ExtractedPatientData } from "../interfaces/extracted-patient.interface"; import { normalizePhoneNumber, normalizeName, normalizeDate, normalizeNumeric, calculateBMI, } from "../utils/smart-corrections"; const BLOOD_GROUPS = [ "A_POSITIVE", "A_NEGATIVE", "B_POSITIVE", "B_NEGATIVE", "AB_POSITIVE", "AB_NEGATIVE", "O_POSITIVE", "O_NEGATIVE", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-", ]; const GENOTYPES = ["AA", "AS", "AC", "SS", "SC", "CC"]; const NIGERIAN_PHONE_REGEX = /^(\+234|0)?[789]\d{9}$/; export const validateGestationalAge = (data: ExtractedPatientData): ValidationWarning | null => { if (data.gestationalAge.value === null) return null; const ga = data.gestationalAge.value; if (ga < 4 || ga > 42) { return { field: "gestationalAge", message: `Gestational age (${ga} weeks) outside expected range (4-42 weeks).`, severity: "error", }; } return null; }; export const validateAge = (data: ExtractedPatientData): ValidationWarning | null => { if (data.age.value === null) return null; const age = data.age.value; if (age < 10 || age > 70) { return { field: "age", message: age < 10 ? "Age appears too young for pregnancy." : "Age appears very high for pregnancy.", severity: "warning", }; } return null; }; export const validateGravidityParity = (data: ExtractedPatientData): ValidationWarning | null => { if (data.gravidity.value === null || data.parity.value === null) return null; if (data.gravidity.value < data.parity.value) { return { field: "gravidity", message: "Gravidity cannot be less than parity.", severity: "error", }; } if (data.age.value !== null && data.age.value <= 15 && data.gravidity.value >= 8) { return { field: "gravidity", message: "Gravidity appears unusually high for this age.", severity: "warning", }; } return null; }; export const validateLivingChildren = (data: ExtractedPatientData): ValidationWarning | null => { if (data.livingChildren.value === null || data.parity.value === null) return null; if (data.livingChildren.value > data.parity.value) { return { field: "livingChildren", message: "Living children cannot exceed parity.", severity: "error", }; } return null; }; export const validatePreviousCaesareans = (data: ExtractedPatientData): ValidationWarning | null => { if (data.previousCaesareans.value === null || data.parity.value === null) return null; if (data.previousCaesareans.value > 0 && data.parity.value === 0) { return { field: "previousCaesareans", message: "Previous caesareans cannot be greater than zero if parity is zero.", severity: "error", }; } return null; }; export const validateBloodGroup = (data: ExtractedPatientData): ValidationWarning | null => { if (data.bloodGroup.value === null) return null; const bg = data.bloodGroup.value.toUpperCase().trim(); if (!BLOOD_GROUPS.includes(bg)) { return { field: "bloodGroup", message: `Blood group "${bg}" is not recognized.`, severity: "warning", }; } return null; }; export const validateGenotype = (data: ExtractedPatientData): ValidationWarning | null => { if (data.genotype.value === null) return null; const gt = data.genotype.value.toUpperCase().trim(); if (!GENOTYPES.includes(gt)) { return { field: "genotype", message: `Genotype "${gt}" is not recognized.`, severity: "warning", }; } return null; }; export const validateLastMenstrualPeriod = (data: ExtractedPatientData): ValidationWarning | null => { if (data.lastMenstrualPeriod.value === null) return null; try { const lmp = new Date(data.lastMenstrualPeriod.value); const today = new Date(); if (lmp > today) { return { field: "lastMenstrualPeriod", message: "Last menstrual period cannot be in the future.", severity: "error", }; } } catch (e) { return { field: "lastMenstrualPeriod", message: "Unable to parse last menstrual period date.", severity: "warning", }; } return null; }; export const validateEstimatedDeliveryDate = ( data: ExtractedPatientData ): ValidationWarning | null => { if (data.estimatedDeliveryDate.value === null) return null; try { const edd = new Date(data.estimatedDeliveryDate.value); const today = new Date(); const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); if (edd < sixMonthsAgo) { return { field: "estimatedDeliveryDate", message: "Estimated delivery date appears to be several months in the past.", severity: "warning", }; } } catch (e) { return { field: "estimatedDeliveryDate", message: "Unable to parse estimated delivery date.", severity: "warning", }; } return null; }; export const validateLmpAndGestationalAge = ( data: ExtractedPatientData ): ValidationWarning | null => { if (data.lastMenstrualPeriod.value === null || data.gestationalAge.value === null) return null; try { const lmp = new Date(data.lastMenstrualPeriod.value); const today = new Date(); const diffMs = today.getTime() - lmp.getTime(); const diffWeeks = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 7)); if (Math.abs(diffWeeks - data.gestationalAge.value) > 4) { return { field: "gestationalAge", message: "Gestational age and last menstrual period do not match.", severity: "warning", }; } } catch { return null; } return null; }; export const validatePhoneNumber = (data: ExtractedPatientData): ValidationWarning | null => { if (data.phone.value === null) return null; const original = data.phone.value; const normalized = normalizePhoneNumber(original); if (!NIGERIAN_PHONE_REGEX.test(normalized)) { return { field: "phone", message: "Phone number does not match expected Nigerian format.", severity: "warning", suggestion: normalized !== original ? normalized : undefined, }; } if (original !== normalized) { return { field: "phone", message: "Possible OCR error detected in phone number.", severity: "info", suggestion: normalized, }; } return null; }; export const validateBloodPressure = (data: ExtractedPatientData): ValidationWarning | null => { if (data.bloodPressure.value === null) return null; const bp = data.bloodPressure.value; const match = bp.match(/(\d+)\s*\/\s*(\d+)/); if (!match) { return { field: "bloodPressure", message: "Unable to parse blood pressure.", severity: "warning", }; } const systolic = parseInt(match[1], 10); const diastolic = parseInt(match[2], 10); if (systolic < 70 || systolic > 200 || diastolic < 40 || diastolic > 130) { return { field: "bloodPressure", message: `Blood pressure (${systolic}/${diastolic}) appears outside expected range.`, severity: "error", }; } return null; }; export const validateWeight = (data: ExtractedPatientData): ValidationWarning | null => { if (data.weight.value === null) return null; const weight = data.weight.value; if (weight < 30 || weight > 200) { return { field: "weight", message: `Weight (${weight} kg) appears outside expected range.`, severity: "warning", }; } return null; }; export const validateHeight = (data: ExtractedPatientData): ValidationWarning | null => { if (data.height.value === null) return null; const height = data.height.value; if (height < 120 || height > 220) { return { field: "height", message: `Height (${height} cm) appears outside expected range.`, severity: "warning", }; } return null; }; export const validateBMI = (data: ExtractedPatientData): ValidationWarning | null => { if (data.weight.value === null || data.height.value === null) return null; const calculatedBMI = calculateBMI(data.weight.value, data.height.value); if (data.bmi.value !== null && Math.abs(data.bmi.value - calculatedBMI) > 2) { return { field: "bmi", message: `BMI (${data.bmi.value}) differs from calculated BMI (${calculatedBMI}).`, severity: "warning", suggestion: String(calculatedBMI), }; } return null; }; export const validateConfidence = (data: ExtractedPatientData): ValidationWarning[] => { const warnings: ValidationWarning[] = []; const fields = Object.keys(data) as Array; fields.forEach((field) => { const fieldData = data[field]; if ( typeof fieldData === "object" && fieldData !== null && "confidence" in fieldData && fieldData.confidence < 0.5 ) { warnings.push({ field: String(field), message: `Low OCR confidence (${Math.round(fieldData.confidence * 100)}%).`, severity: "warning", }); } }); return warnings; }; export const ALL_RULES = [ validateGestationalAge, validateAge, validateGravidityParity, validateLivingChildren, validatePreviousCaesareans, validateBloodGroup, validateGenotype, validateLastMenstrualPeriod, validateEstimatedDeliveryDate, validateLmpAndGestationalAge, validatePhoneNumber, validateBloodPressure, validateWeight, validateHeight, validateBMI, ];