Spaces:
Running
Running
| 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<keyof ExtractedPatientData>; | |
| 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, | |
| ]; | |