diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index e862acc1..04bf42d7 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -354,6 +354,30 @@ function parseSrpCode(input: string): CdtMatch | null { return { code: codeStr, description: row.Description, input, quad: quadrant }; } +/** + * Parse re-cement wording regardless of word order or tooth-number format: + * "recement crown #7", "#7 crown recement", "recement crown 7". "recement" + * outranks "crown" whenever both appear — this is a re-cement (D2920), not + * a new crown (D2740). + */ +function parseRecementCode(input: string): CdtMatch | null { + const lower = input.toLowerCase(); + if (!/\bre[\s-]?cement(?:ed|ing)?\b/.test(lower)) return null; + + let toothNumber: string | undefined; + const hashMatch = input.match(/#\s*(\d{1,2})\b/); + const toothWordMatch = input.match(/\btooth\s+(\d{1,2})\b/i); + const bareNumMatch = input.match(/\b(\d{1,2})\b/); + const m = hashMatch ?? toothWordMatch ?? bareNumMatch; + if (m) { + const n = parseInt(m[1]!, 10); + if (n >= 1 && n <= 32) toothNumber = String(n); + } + + const row = ALL_CODES.find((r) => r["Procedure Code"] === "D2920"); + return { code: "D2920", description: row?.Description ?? "D2920", input, ...(toothNumber ? { toothNumber } : {}) }; +} + /** * Parse RCT/root-canal notation like "rct #29", "#14 rct", "root canal #6". * Returns D3310 (anterior), D3320 (premolar), or D3330 (molar) based on tooth number. @@ -545,23 +569,28 @@ export function lookupCdtCodes( return { code, description: row?.Description ?? code, input: name, ...toothInfo }; } - // 2. Multi-PA shorthand: "4PA", "4PA #3 #14 #19 #30", etc. + // 2. Re-cement wording — outranks "crown"/etc. elsewhere in the same entry, + // regardless of word order or tooth-number format (e.g. "#7 crown recement"). + const recementMatch = parseRecementCode(name); + if (recementMatch) return recementMatch; + + // 3. Multi-PA shorthand: "4PA", "4PA #3 #14 #19 #30", etc. const multiPaMatches = parseMultiPaCode(name); if (multiPaMatches) return multiPaMatches; - // 3. Composite filling by tooth# and surfaces — also determines CDT code from tooth position + // 4. Composite filling by tooth# and surfaces — also determines CDT code from tooth position const compositeMatch = parseCompositeCode(name); if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface - // 4. RCT by tooth# — D3310/D3320/D3330 based on tooth position + // 5. RCT by tooth# — D3310/D3320/D3330 based on tooth position const rctMatch = parseRctCode(name); if (rctMatch) return rctMatch; - // 5. SRP + quadrant — D4341/D4342 with UL/UR/LL/LR + // 6. SRP + quadrant — D4341/D4342 with UL/UR/LL/LR const srpMatch = parseSrpCode(name); if (srpMatch) return srpMatch; - // 6. Hardcoded direct code map (short abbreviations / codes not in MH schedule) + // 7. Hardcoded direct code map (short abbreviations / codes not in MH schedule) const directKey = DIRECT_CODE_MAP[cleaned] ? cleaned : DIRECT_CODE_MAP[strippedCleaned] ? strippedCleaned : null; if (directKey) { const code = DIRECT_CODE_MAP[directKey]!; @@ -569,7 +598,7 @@ export function lookupCdtCodes( return { code, description: row?.Description ?? code, input: name, ...toothInfo }; } - // 7. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching) + // 8. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching) const match = matchOne(strippedCleaned !== cleaned ? strippedCleaned : name); if (match) return { ...match, input: name, ...toothInfo }; diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 5e124d59..fd441b94 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -51,6 +51,9 @@ import { applyComboToForm, getDescriptionForCode, findPriceMismatches, + getCodeMap, + getPriceForCodeWithAgeFromMap, + ageOnDate, type PriceMismatch, } from "@/utils/procedureCombosMapping"; import { PROCEDURE_COMBOS } from "@/utils/procedureCombos"; @@ -624,7 +627,11 @@ export function ClaimForm({ } }); const filled = { ...prev, insuranceSiteKey, serviceLines: updatedLines }; - return mapPricesForForm({ form: filled, patientDOB: "", insuranceSiteKey }); + return mapPricesForForm({ + form: filled, + patientDOB: patient?.dateOfBirth ?? "", + insuranceSiteKey, + }); }); setPrefillDone(true); } catch {} @@ -845,6 +852,22 @@ export function ClaimForm({ } else { updatedLines[index][field] = value; } + + // Auto-fill the price as soon as a recognized, fully-typed CDT code is entered, + // so users don't have to remember to click "Map Price" for a single line. + if (field === "procedureCode") { + const code = String(value || "").trim().toUpperCase(); + if (/^D\d{4}$/.test(code)) { + const line = updatedLines[index]; + const map = getCodeMap(form.insuranceSiteKey); + const age = ageOnDate( + patient?.dateOfBirth ?? "", + line.procedureDate || form.serviceDate, + ); + const price = getPriceForCodeWithAgeFromMap(map, code, age); + updatedLines[index] = { ...line, totalBilled: price }; + } + } } setForm({ ...form, serviceLines: updatedLines }); @@ -1873,7 +1896,17 @@ export function ClaimForm({ } else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") { handleUnitedDHPreAuth(); } else { - handleMHPreAuth(); + // Re-apply fee-schedule prices with the now-loaded patient DOB before submitting — + // the earlier chatbot-prefill pass may have run before `patient` finished loading, + // leaving age-tiered codes (e.g. D2740) unpriced. + const siteKeyForPricing = + form.insuranceSiteKey || deriveInsuranceSiteKey(form.insuranceProvider || ""); + const pricedForm = mapPricesForForm({ + form: { ...form, insuranceSiteKey: siteKeyForPricing }, + patientDOB: patient?.dateOfBirth ?? "", + insuranceSiteKey: siteKeyForPricing, + }); + handleMHPreAuth(pricedForm); } }, [autoSubmitPreauth, prefillDone, isFormReady]); diff --git a/apps/Frontend/src/utils/procedureCombos.ts b/apps/Frontend/src/utils/procedureCombos.ts index e9dbaabd..a418d515 100755 --- a/apps/Frontend/src/utils/procedureCombos.ts +++ b/apps/Frontend/src/utils/procedureCombos.ts @@ -303,6 +303,11 @@ export const PROCEDURE_COMBOS: Record< label: "Crown", codes: ["D2740"], }, + recementCrown: { + id: "recementCrown", + label: "Recement Crown", + codes: ["D2920"], + }, deepCleaning: { id: "deepCleaning", label: "Deep Cleaning", @@ -396,7 +401,7 @@ export const COMBO_CATEGORIES: Record< ], Implants: ["implantFull", "implantFixture", "implantAbutment", "implantCrown"], Endodontics: ["rctAnterior", "rctAnteriorPostCrown", "rctPremolar", "rctPremolarPostCrown", "rctMolar", "rctMolarPostCrown", "postCore", "coreBU"], - Prosthodontics: ["crown"], + Prosthodontics: ["crown", "recementCrown"], Periodontics: ["deepCleaning"], Extractions: [ "simpleExtraction", diff --git a/apps/Frontend/src/utils/procedureCombosMapping.ts b/apps/Frontend/src/utils/procedureCombosMapping.ts index c1a65d78..f4d7dbdc 100755 --- a/apps/Frontend/src/utils/procedureCombosMapping.ts +++ b/apps/Frontend/src/utils/procedureCombosMapping.ts @@ -150,7 +150,7 @@ const parseDate = (d: DateInput): Date => { return new Date(s); }; -const ageOnDate = (dob: DateInput, on: DateInput): number => { +export const ageOnDate = (dob: DateInput, on: DateInput): number => { const birth = parseDate(dob); const ref = parseDate(on); let age = ref.getFullYear() - birth.getFullYear(); diff --git a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py index 2f4f5479..b1c7bbe6 100755 --- a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py @@ -637,14 +637,6 @@ class AutomationDeltaDentalMAEligibilityCheck: print(f"[DDMA step2] PDF saved at: {pdf_path}") - # Close the browser window after PDF (session preserved in profile) - try: - from ddma_browser_manager import get_browser_manager - get_browser_manager().quit_driver() - print("[step2] Browser closed - session preserved in profile") - except Exception as e: - print(f"[step2] Error closing browser: {e}") - print(f"[DDMA step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'") return {