import { InputServiceLine } from "@repo/db/types"; import Decimal from "decimal.js"; import rawCodeTable from "@/assets/data/procedureCodesMH.json"; import rawCCACodeTable from "@/assets/data/procedureCodesCCA.json"; import rawDDMACodeTable from "@/assets/data/procedureCodesDDMA.json"; import rawUnitedDHCodeTable from "@/assets/data/procedureCodesUnitedDH.json"; import rawUnitedUcardCodeTable from "@/assets/data/procedureCodesUnitedUcard.json"; import rawTuftsSCOCodeTable from "@/assets/data/procedureCodesTuftsSCO.json"; import rawDeltainsCodeTable from "@/assets/data/procedureCodesDeltains.json"; import rawBCBSCodeTable from "@/assets/data/procedureCodesBCBS.json"; import rawAltusCodeTable from "@/assets/data/procedureCodesAltus.json"; import rawMetLifeCodeTable from "@/assets/data/procedureCodesMetLife.json"; import { PROCEDURE_COMBOS } from "./procedureCombos"; /* ----------------------------- Types ----------------------------- */ export type CodeRow = { "Procedure Code": string; Description?: string; Price?: string | number | null; PriceLTEQ21?: string | number | null; PriceGT21?: string | number | null; [k: string]: unknown; }; const CODE_TABLE = rawCodeTable as CodeRow[]; const CCA_CODE_TABLE = rawCCACodeTable as CodeRow[]; const DDMA_CODE_TABLE = rawDDMACodeTable as CodeRow[]; const UNITEDDH_CODE_TABLE = rawUnitedDHCodeTable as CodeRow[]; const UNITEDUCARD_CODE_TABLE = rawUnitedUcardCodeTable as CodeRow[]; const TUFTSSCO_CODE_TABLE = rawTuftsSCOCodeTable as CodeRow[]; const DELTAINS_CODE_TABLE = rawDeltainsCodeTable as CodeRow[]; const BCBS_CODE_TABLE = rawBCBSCodeTable as CodeRow[]; const ALTUS_CODE_TABLE = rawAltusCodeTable as CodeRow[]; const METLIFE_CODE_TABLE = rawMetLifeCodeTable as CodeRow[]; export type ClaimFormLike = { serviceDate: string; // form-level service date serviceLines: InputServiceLine[]; }; export type ApplyOptions = { append?: boolean; startIndex?: number; lineDate?: string; clearTrailing?: boolean; replaceAll?: boolean; skipPrice?: boolean; }; /* ----------------------------- Helpers ----------------------------- */ export const COMBO_BUTTONS = Object.values(PROCEDURE_COMBOS).map((c) => ({ id: c.id, label: c.label, })); // Build a fast lookup map keyed by normalized code const normalizeCode = (code: string) => code.replace(/\s+/g, "").toUpperCase(); const CODE_MAP: Map = (() => { const m = new Map(); for (const r of CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const CCA_CODE_MAP: Map = (() => { const m = new Map(); for (const r of CCA_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const DDMA_CODE_MAP: Map = (() => { const m = new Map(); for (const r of DDMA_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const BCBS_CODE_MAP: Map = (() => { const m = new Map(); for (const r of BCBS_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const ALTUS_CODE_MAP: Map = (() => { const m = new Map(); for (const r of ALTUS_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const METLIFE_CODE_MAP: Map = (() => { const m = new Map(); for (const r of METLIFE_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const UNITEDDH_CODE_MAP: Map = (() => { const m = new Map(); for (const r of UNITEDDH_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const TUFTSSCO_CODE_MAP: Map = (() => { const m = new Map(); for (const r of TUFTSSCO_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const UNITEDUCARD_CODE_MAP: Map = (() => { const m = new Map(); for (const r of UNITEDUCARD_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); const DELTAINS_CODE_MAP: Map = (() => { const m = new Map(); for (const r of DELTAINS_CODE_TABLE) { const k = normalizeCode(String(r["Procedure Code"] || "")); if (k && !m.has(k)) m.set(k, r); } return m; })(); /** Return the correct fee-schedule map for the given insurance type. */ function getCodeMap(insuranceSiteKey?: string): Map { const k = (insuranceSiteKey ?? "").replace(/_/g, "").toLowerCase(); if (k === "cca") return CCA_CODE_MAP; if (k === "ddma") return DDMA_CODE_MAP; if (k === "bcbs" || k === "bcbsma") return BCBS_CODE_MAP; if (k === "altus") return ALTUS_CODE_MAP; if (k === "metlife" || k === "metlifedental") return METLIFE_CODE_MAP; if (k === "deltains") return DELTAINS_CODE_MAP; if (k === "unitedsco" || k === "uniteddh" || k === "dentalhub") return UNITEDDH_CODE_MAP; if (k === "uniteducard" || k === "ucard") return UNITEDUCARD_CODE_MAP; if (k === "tuftssco" || k === "tufts") return TUFTSSCO_CODE_MAP; return CODE_MAP; // default: MassHealth } // this function is solely for abbrevations feature in claim-form export function getDescriptionForCode( code: string | undefined ): string | undefined { if (!code) return undefined; const row = CODE_MAP.get(normalizeCode(code)); return row?.Description; } const isBlankPrice = (v: unknown) => { if (v == null) return true; const s = String(v).trim().toUpperCase(); return s === "" || s === "IC" || s === "NC"; }; const toDecimalOrZero = (v: unknown): Decimal => { if (isBlankPrice(v)) return new Decimal(0); const n = typeof v === "string" ? parseFloat(v) : (v as number); return new Decimal(Number.isFinite(n) ? n : 0); }; // Accepts string or Date, supports MM/DD/YYYY and YYYY-MM-DD type DateInput = string | Date; const parseDate = (d: DateInput): Date => { if (d instanceof Date) return new Date(d.getFullYear(), d.getMonth(), d.getDate()); const s = String(d).trim(); // MM/DD/YYYY const mdy = /^(\d{2})\/(\d{2})\/(\d{4})$/; const m1 = mdy.exec(s); if (m1) { const mm = Number(m1[1]); const dd = Number(m1[2]); const yyyy = Number(m1[3]); return new Date(yyyy, mm - 1, dd); } // YYYY-MM-DD const ymd = /^(\d{4})-(\d{2})-(\d{2})$/; const m2 = ymd.exec(s); if (m2) { const yyyy = Number(m2[1]); const mm = Number(m2[2]); const dd = Number(m2[3]); return new Date(yyyy, mm - 1, dd); } // Fallback return new Date(s); }; export const ageOnDate = (dob: DateInput, on: DateInput): number => { const birth = parseDate(dob); const ref = parseDate(on); let age = ref.getFullYear() - birth.getFullYear(); const hadBirthday = ref.getMonth() > birth.getMonth() || (ref.getMonth() === birth.getMonth() && ref.getDate() >= birth.getDate()); if (!hadBirthday) age -= 1; return age; }; /** * we can implement per-code age buckets without changing the JSON. * * Behavior: * - Default: same as before: age <= 21 -> PriceLTEQ21, else PriceGT21 * - Fallback to Price if tiered field is blank/IC/NC * - Special-cases D1110 and D1120 according to MH rules */ export function pickPriceForRowByAge( row: CodeRow, age: number, normalizedCode?: string ): Decimal { // Special-case rules (add more codes here if needed) if (normalizedCode) { // D1110: only valid for age >=14 if (normalizedCode === "D1110") { if (age < 14) return new Decimal(0); // D1110 not for children <14 // age >= 14: use age-split if present, then flat Price if (age <= 21 && !isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21); if (age > 21 && !isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); return new Decimal(0); } // D1120: valid for child 0-13 only if (normalizedCode === "D1120") { if (age >= 14) return new Decimal(0); // NC for adults if (!isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21); if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); return new Decimal(0); } } // Generic/default: age-split first, flat Price as fallback if (age <= 21 && !isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21); if (age > 21 && !isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); return new Decimal(0); } /** * Gets price for a code using age & code table. */ function getPriceForCodeWithAgeFromMap( map: Map, code: string, age: number ): Decimal { const norm = normalizeCode(code); const row = map.get(norm); return row ? pickPriceForRowByAge(row, age, norm) : new Decimal(0); } // helper keeping lines empty, export const makeEmptyLine = (lineDate: string): InputServiceLine => ({ procedureCode: "", procedureDate: lineDate, quad: "", arch: "", toothNumber: "", toothSurface: "", totalBilled: new Decimal(0), totalAdjusted: new Decimal(0), totalPaid: new Decimal(0), }); // Ensure the array has at least `min` lines; append blank ones if needed. const ensureCapacity = ( lines: (InputServiceLine | undefined)[], min: number, lineDate: string ) => { while (lines.length < min) { lines.push(makeEmptyLine(lineDate)); } }; /* ------------------------- Main entry points ------------------------- */ /** * Map prices for ALL existing lines in a form (your "Map Price" button), * using patient's DOB and the form's serviceDate (or per-line procedureDate). * Returns a NEW form object (immutable). */ export function mapPricesForForm(params: { form: T; patientDOB: DateInput; insuranceSiteKey?: string; }): T { const { form, patientDOB, insuranceSiteKey } = params; const map = getCodeMap(insuranceSiteKey); return { ...form, serviceLines: form.serviceLines.map((ln) => { const age = ageOnDate(patientDOB, form.serviceDate); const code = normalizeCode(ln.procedureCode || ""); if (!code) return { ...ln }; const price = getPriceForCodeWithAgeFromMap(map, code, age); return { ...ln, procedureCode: code, totalBilled: price }; }), }; } /** * Apply a preset combo (fills codes & prices) using patientDOB and serviceDate. * Returns a NEW form object (immutable). */ export function applyComboToForm( form: T, comboId: keyof typeof PROCEDURE_COMBOS, patientDOB: DateInput, options: ApplyOptions = {}, insuranceSiteKey?: string ): T { const preset = PROCEDURE_COMBOS[String(comboId)]; if (!preset) return form; const { append = true, startIndex, lineDate = form.serviceDate, clearTrailing = false, replaceAll = false, // NEW } = options; const next: T = { ...form, serviceLines: [...form.serviceLines] }; // Replace-all: blank all existing and start from 0 if (replaceAll) { for (let i = 0; i < next.serviceLines.length; i++) { next.serviceLines[i] = makeEmptyLine(lineDate); } } // determine insertion index let insertAt = 0; if (!replaceAll) { if (append) { let last = -1; next.serviceLines.forEach((ln, i) => { if (ln.procedureCode?.trim()) last = i; }); insertAt = Math.max(0, last + 1); } else if (typeof startIndex === "number") { insertAt = Math.max( 0, Math.min(startIndex, next.serviceLines.length - 1) ); } } // if replaceAll, insertAt stays 0 // Make sure we have enough rows for the whole combo ensureCapacity(next.serviceLines, insertAt + preset.codes.length, lineDate); const age = options.skipPrice ? 0 : ageOnDate(patientDOB, lineDate); const map = options.skipPrice ? CODE_MAP : getCodeMap(insuranceSiteKey); for (let j = 0; j < preset.codes.length; j++) { const i = insertAt + j; if (i >= next.serviceLines.length) break; const codeRaw = preset.codes[j]; if (!codeRaw) continue; const code = normalizeCode(codeRaw); const price = options.skipPrice ? new Decimal(0) : getPriceForCodeWithAgeFromMap(map, code, age); const original = next.serviceLines[i]; next.serviceLines[i] = { ...original, procedureCode: code, procedureDate: lineDate, quad: (original as any)?.quad ?? "", arch: (original as any)?.arch ?? "", toothNumber: preset.toothNumbers?.[j] ?? (original as any)?.toothNumber ?? "", toothSurface: (original as any)?.toothSurface ?? "", totalBilled: price, totalAdjusted: new Decimal(0), totalPaid: new Decimal(0), } as InputServiceLine; } if (replaceAll || clearTrailing) { const after = insertAt + preset.codes.length; for (let i = after; i < next.serviceLines.length; i++) { next.serviceLines[i] = makeEmptyLine(lineDate); } } return next; } export { CODE_MAP, CCA_CODE_MAP, DDMA_CODE_MAP, UNITEDDH_CODE_MAP, UNITEDUCARD_CODE_MAP, TUFTSSCO_CODE_MAP, getCodeMap, getPriceForCodeWithAgeFromMap }; export type PriceMismatch = { procedureCode: string; enteredPrice: number; schedulePrice: number; }; /** Compare each service line's totalBilled against the fee schedule. * Returns lines where the entered price differs from the schedule price. * Returns empty array if the siteKey has no schedule (United, Tufts, etc.). */ export function findPriceMismatches( serviceLines: InputServiceLine[], insuranceSiteKey: string | undefined, patientDOB: string, serviceDate: string, ): PriceMismatch[] { const supported = ["MH", "MASSHEALTH", "CCA", "DDMA", "UNITEDDH", "UNITEDSCO", "UNITED_UCARD", "UNITEDUCARD", "TUFTSSCO", "METLIFE", "METLIFEDENTAL"]; if (!insuranceSiteKey || !supported.includes(insuranceSiteKey.toUpperCase())) return []; const map = getCodeMap(insuranceSiteKey); const mismatches: PriceMismatch[] = []; for (const line of serviceLines) { const code = normalizeCode(line.procedureCode || ""); if (!code) continue; const enteredPrice = new Decimal(Number(line.totalBilled) || 0); if (enteredPrice.isZero()) continue; const age = ageOnDate(patientDOB, serviceDate); const schedulePrice = getPriceForCodeWithAgeFromMap(map, code, age); if (!schedulePrice.isZero() && !enteredPrice.equals(schedulePrice)) { mismatches.push({ procedureCode: code, enteredPrice: enteredPrice.toNumber(), schedulePrice: schedulePrice.toNumber(), }); } } return mismatches; }