Compare commits

..
2 Commits
Author SHA1 Message Date
GiteadandClaude Sonnet 5 e8889c0ff8 fix: auto-price CDT codes and fix crown/recement code mismatches
- Auto-fill service line price when a recognized CDT code is typed
  (e.g. D2740), instead of requiring a separate "Map Price" click
- Fix chatbot preauth prefill using an empty DOB for price lookup,
  causing age-tiered codes (D2740) to price as $0
- Re-apply fee-schedule pricing with the loaded patient DOB before
  MassHealth preauth auto-submit
- Add a "Recement Crown" (D2920) combo button distinct from "Crown"
  (D2740) to avoid picking the wrong code/price
- Fix AI note parsing to prioritize "recement" over "crown" regardless
  of word order or tooth-number format (e.g. "#7 crown recement")

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 23:29:43 -04:00
GiteadandClaude Sonnet 5 00a81b3fda fix: correctly suppress PDF popup for last claim in AI-column batch
closeClaim() was clearing chatbotAutoSubmitSiteKey synchronously right
after the selenium job kicked off, before the job's async completion
handler could check it. This was masked for all but the last claim in
the batch because closeClaim() also navigated away (unmounting the
page) whenever more claims were queued. On the last claim the page
stayed mounted, so the completion handler read a stale cleared value
and let the PDF preview popup through. Track the site key in a ref
that survives closeClaim() and is cleared only after the job's own
completion handler consumes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 08:56:52 -04:00
6 changed files with 87 additions and 20 deletions
+35 -6
View File
@@ -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 };
@@ -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]);
+10 -2
View File
@@ -44,6 +44,10 @@ export default function ClaimsPage() {
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
// Mirrors chatbotAutoSubmitSiteKey but survives closeClaim(), which resets the state
// synchronously right after the selenium job is kicked off — well before the job's async
// completion handlers (which gate the PDF preview popup) actually run.
const chatbotAutoSubmitSiteKeyRef = useRef<string | undefined>(undefined);
const [chatbotInitialTab, setChatbotInitialTab] = useState<string | undefined>(undefined);
const [chatbotAutoSubmitPreauthSiteKey, setChatbotAutoSubmitPreauthSiteKey] = useState<string | undefined>(undefined);
// for redirect from appointment page directly, then passing to claimform
@@ -156,11 +160,12 @@ export default function ClaimsPage() {
if (jobResult.pdfFileId) {
advanceAiClaimQueue();
const advancedToNext = advanceChatbotClaimQueue();
if (!advancedToNext && !chatbotAutoSubmitSiteKey) {
if (!advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
setPreviewPdfId(jobResult.pdfFileId);
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
setPreviewOpen(true);
}
chatbotAutoSubmitSiteKeyRef.current = undefined;
dispatch(setTaskStatus({
key: "claimSubmit",
status: "success",
@@ -240,6 +245,7 @@ export default function ClaimsPage() {
const parsed = JSON.parse(raw);
if (parsed?.autoSubmit && parsed?.siteKey) {
setChatbotAutoSubmitSiteKey(parsed.siteKey);
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
}
}
} catch {}
@@ -308,6 +314,7 @@ export default function ClaimsPage() {
const parsed = JSON.parse(raw);
if (parsed?.autoSubmit && parsed?.siteKey) {
setChatbotAutoSubmitSiteKey(parsed.siteKey);
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
}
}
} catch {}
@@ -994,11 +1001,12 @@ export default function ClaimsPage() {
// Pop up the final PDF so the user doesn't need to navigate to Documents
// (skip if we're about to navigate to the next queued patient's claim,
// or if this claim was submitted via the AI chat column)
if (result.pdfFileId && !advancedToNext && !chatbotAutoSubmitSiteKey) {
if (result.pdfFileId && !advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
setPreviewPdfId(result.pdfFileId);
setPreviewFallbackFilename(result.fileName ?? null);
setPreviewOpen(true);
}
chatbotAutoSubmitSiteKeyRef.current = undefined;
return result;
} catch (error: any) {
+6 -1
View File
@@ -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",
@@ -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();
@@ -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 {