import { implausibleResult } from '../labs/targets.js'; import type { CheckLab } from './engine.js'; import type { EngineHooks, KidneyDoseLine } from './options.js'; import { SPECIFIC_REFERENCES } from './sources.js'; /** * CHK-11 (kidneyDoseLimits): metformin's kidney-adjusted maximum from the Glucophage 500 mg UK SmPC (Merck) 4.2 and 4.3, * read 30 October 2026 (revised November 2025). The SmPC gives GFR in mL/min; the engine compares eGFR, as its lab rules do. */ export const METFORMIN_KIDNEY_BANDS: ReadonlyArray<{ band: string; from: number; below: number | null; maxDailyMg: number | null }> = [ { band: 'under 30', from: -Infinity, below: 30, maxDailyMg: null }, { band: '30-44', from: 30, below: 45, maxDailyMg: 1000 }, { band: '45-59', from: 45, below: 60, maxDailyMg: 2000 }, // 60 and over: the adult maximum (3000 mg) applies, which the usual dose check already uses. ]; const REFERENCE = SPECIFIC_REFERENCES['kidney-dose-metformin']![0]; /** A result older than this isn't used, as the lab rules don't use it (engine STALE_RESULT_DAYS). */ const STALE_DAYS = 90; const dateOf = (iso: string) => new Date(`${iso.slice(0, 10)}T00:00:00Z`).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }); /** The band a result is in; for a value printed as a bound, the band only when every value it allows is in it. */ function bandOf(lab: Pick) { const low = lab.comparator === '<' || lab.comparator === '<=' ? -Infinity : lab.value; const high = lab.comparator === '>' || lab.comparator === '>=' ? Infinity : lab.value; if (low >= 60) return 'none' as const; return METFORMIN_KIDNEY_BANDS.find((band) => low >= band.from && (band.below === null || high < band.below || (high === band.below && lab.comparator === '<'))) ?? ('unclear' as const); } /** The hook the engine calls for each medicine's dose line (EngineHooks.kidneyDose). */ export function kidneyDoseHook(labs: readonly CheckLab[], today: string, calculatedLab: object | null): NonNullable { const egfr = [...labs].filter((lab) => lab.code === 'egfr').sort((x, y) => x.observedAt.localeCompare(y.observedAt)).pop(); return ({ key, total }) => { if (key !== 'metformin' || !egfr) return null; const ageDays = (Date.parse(`${today}T00:00:00Z`) - Date.parse(egfr.observedAt)) / 86_400_000; if (ageDays > STALE_DAYS || implausibleResult('egfr', egfr.value)) return null; const band = bandOf(egfr); if (band === 'none') return null; const printed = `${egfr.comparator ?? ''}${egfr.value}`; const latest = `latest eGFR ${printed}${calculatedLab === egfr ? ', calculated from creatinine' : ''}, ${dateOf(egfr.observedAt)}`; const base = { ingredient: 'metformin', egfr: egfr.value, egfr_comparator: egfr.comparator ?? null, egfr_observed_at: egfr.observedAt, egfr_calculated: calculatedLab === egfr, daily_mg: total, source_id: REFERENCE.sourceId, section: REFERENCE.section }; if (band === 'unclear') { return { status: 'check_with_pharmacist', contraindicated: false, kidney_limit: { ...base, band: 'unclear', max_daily_mg: null }, plain: `The latest eGFR was printed as "${printed}", so the label's kidney-adjusted maximum for metformin couldn't be worked out.` } satisfies KidneyDoseLine; } const limit = { ...base, band: band.band, max_daily_mg: band.maxDailyMg }; if (band.maxDailyMg === null) { return { status: 'above_limit', contraindicated: true, kidney_limit: limit, plain: `The metformin label lists metformin as contraindicated below an eGFR of 30 (${latest}).` }; } if (total === null) return null; const max = band.maxDailyMg.toLocaleString('en-GB'); if (total > band.maxDailyMg) { return { status: 'above_limit', contraindicated: false, kidney_limit: limit, plain: `About ${Math.round(total)} mg of metformin a day is more than the label's ${max} mg maximum for an eGFR of ${band.band.replace('-', ' to ')} (${latest}).` }; } return { status: 'within_usual_range', contraindicated: false, kidney_limit: limit, plain: `The daily amount entered does not exceed the label's ${max} mg maximum for an eGFR of ${band.band.replace('-', ' to ')} (${latest}). ` + 'This does not confirm the right dose to take.' }; }; }