feat: MetLife Dental claim submission via ClaimConnect; MetLife fee schedule

Reuses the MetLife eligibility login (persistent profile, email MFA, OTP
from app or browser), then Submit Claims -> ClaimConnect -> DentalXChange
Single Claim: billing/rendering provider, member ID, last name, payer
Metlife 65978, patient picked by DOB/name, signature on file Yes, one
claim item per CDT code (date, code, tooth, surfaces, fee; Add Claim Item
past 3), Finish, Submit Claim, then Back to Claim to read the Claim ID and
save the page as PDF. Waits out Wicket ajax/veil before each click.

MetLife now has its own fee schedule (seeded from DDMA) instead of falling
back to MassHealth prices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 00:01:39 -04:00
co-authored by Claude Opus 5.5
parent db7d74a13b
commit a5beeb7ba9
15 changed files with 1793 additions and 1 deletions
@@ -0,0 +1,209 @@
[
{ "Procedure Code": "D0120", "Description": "Periodic oral evaluation - established patient", "Price": 105 },
{ "Procedure Code": "D0140", "Description": "Limited oral evaluation - problem focused", "Price": 90 },
{ "Procedure Code": "D0145", "Description": "Oral evaluation for a patient under three years of age and counseling with primary caregiver", "Price": "NC" },
{ "Procedure Code": "D0150", "Description": "Comprehensive oral evaluation - new or established patient", "Price": 120 },
{ "Procedure Code": "D0180", "Description": "Comprehensive periodontal evaluation - new or established patient", "Price": 37 },
{ "Procedure Code": "D0190", "Description": "Screening of a patient (PHDH only)", "Price": 20 },
{ "Procedure Code": "D0191", "Description": "Assessment of a patient (PHDH only)", "Price": 20 },
{ "Procedure Code": "D0210", "Description": "Intraoral - complete series of radiographic images", "Price": 120 },
{ "Procedure Code": "D0220", "Description": "Intraoral - periapical, first radiographic image", "Price": 60 },
{ "Procedure Code": "D0230", "Description": "Intraoral - periapical, each additional radiographic image", "Price": 50 },
{ "Procedure Code": "D0240", "Description": "Intraoral - occlusal radiographic image", "Price": "NC" },
{ "Procedure Code": "D0270", "Description": "Bitewing - single radiographic image", "Price": 14 },
{ "Procedure Code": "D0272", "Description": "Bitewings - two radiographic images", "Price": 80 },
{ "Procedure Code": "D0273", "Description": "Bitewings - three radiographic images", "Price": 27 },
{ "Procedure Code": "D0274", "Description": "Bitewings - four radiographic images", "Price": 160 },
{ "Procedure Code": "D0330", "Description": "Panoramic radiographic image", "Price": 150 },
{ "Procedure Code": "D0340", "Description": "Cephalometric radiograph image (Oral surgeon only)", "Price": 74 },
{ "Procedure Code": "D0364", "Description": "CBCT - less than one jaw (interpret)", "Price": 350 },
{ "Procedure Code": "D0365", "Description": "CBCT - mandible (interpret)", "Price": 350 },
{ "Procedure Code": "D0366", "Description": "CBCT - maxilla (interpret)", "Price": 350 },
{ "Procedure Code": "D0367", "Description": "CBCT - both jaws (interpret)", "Price": 400 },
{ "Procedure Code": "D0368", "Description": "CBCT - include TMJ (interpret)", "Price": 375 },
{ "Procedure Code": "D0380", "Description": "CBCT - less than one jaw (capture only)", "Price": 300 },
{ "Procedure Code": "D0381", "Description": "CBCT - mandible (capture only)", "Price": 300 },
{ "Procedure Code": "D0382", "Description": "CBCT - maxilla (capture only)", "Price": 300 },
{ "Procedure Code": "D0383", "Description": "CBCT - both jaws / include TMJ (capture only)", "Price": 350 },
{ "Procedure Code": "D1110", "Description": "Prophylaxis - adult, 14 yo or older", "Price": 150 },
{ "Procedure Code": "D1120", "Description": "Prophylaxis - child, 0-13 yo", "Price": 120 },
{ "Procedure Code": "D1206", "Description": "Topical application of fluoride varnish", "Price": 26 },
{ "Procedure Code": "D1208", "Description": "Topical application of fluoride - excluding varnish", "Price": 90 },
{ "Procedure Code": "D1351", "Description": "Sealant - per tooth", "Price": 80 },
{ "Procedure Code": "D1354", "Description": "Application of caries arresting medicament - per tooth", "Price": 15 },
{ "Procedure Code": "D1510", "Description": "Space maintainer - fixed, unilateral - per quadrant", "Price": "NC" },
{ "Procedure Code": "D1516", "Description": "Space maintainer - fixed, bilateral, maxillary", "Price": "NC" },
{ "Procedure Code": "D1517", "Description": "Space maintainer - fixed, bilateral, mandibular", "Price": "NC" },
{ "Procedure Code": "D1520", "Description": "Space maintainer - removable, unilateral, per quadrant", "Price": "NC" },
{ "Procedure Code": "D1526", "Description": "Space maintainer - removable, bilateral, maxillary", "Price": "NC" },
{ "Procedure Code": "D1527", "Description": "Space maintainer - removable, bilateral, mandibular", "Price": "NC" },
{ "Procedure Code": "D1575", "Description": "Distal shoe space maintainer - fixed, unilateral, per quadrant", "Price": "NC" },
{ "Procedure Code": "D1701", "Description": "Pfizer-BioNTech Covid-19 vaccine - first dose", "Price": 45.87 },
{ "Procedure Code": "D1702", "Description": "Pfizer-BioNTech Covid-19 vaccine - second dose", "Price": 45.87 },
{ "Procedure Code": "D1707", "Description": "Janssen Covid-19 vaccine - single dose", "Price": 45.87 },
{ "Procedure Code": "D1708", "Description": "Pfizer-BioNTech Covid-19 vaccine - third dose", "Price": 45.87 },
{ "Procedure Code": "D1709", "Description": "Pfizer-BioNTech Covid-19 vaccine - booster dose", "Price": 45.87 },
{ "Procedure Code": "D1712", "Description": "Janssen Covid-19 vaccine - booster dose", "Price": 45.87 },
{ "Procedure Code": "D1713", "Description": "Pfizer-BioNTech Covid-19 vaccine pediatric - first dose", "Price": 45.87 },
{ "Procedure Code": "D1714", "Description": "Pfizer-BioNTech Covid-19 vaccine pediatric - second dose", "Price": 45.87 },
{ "Procedure Code": "D1999", "Description": "Unspecified preventive procedure, by report", "Price": 50 },
{ "Procedure Code": "D2140", "Description": "Amalgam - one surface, primary or permanent", "Price": 150 },
{ "Procedure Code": "D2150", "Description": "Amalgam - two surfaces, primary or permanent", "Price": 200 },
{ "Procedure Code": "D2160", "Description": "Amalgam - three surfaces, primary or permanent", "Price": 92 },
{ "Procedure Code": "D2161", "Description": "Amalgam - four or more surfaces, primary or permanent", "Price": 116 },
{ "Procedure Code": "D2330", "Description": "Resin-based composite - one surface, anterior", "Price": 180 },
{ "Procedure Code": "D2331", "Description": "Resin-based composite - two surfaces, anterior", "Price": 220 },
{ "Procedure Code": "D2332", "Description": "Resin-based composite - three surfaces, anterior", "Price": 280 },
{ "Procedure Code": "D2335", "Description": "Resin-based composite - four or more surfaces or involving incisal angle (anterior)", "Price": 350 },
{ "Procedure Code": "D2390", "Description": "Resin-based composite crown, anterior", "Price": "NC" },
{ "Procedure Code": "D2391", "Description": "Resin-based composite - one surface, posterior", "Price": 200 },
{ "Procedure Code": "D2392", "Description": "Resin-based composite - two surfaces, posterior", "Price": 250 },
{ "Procedure Code": "D2393", "Description": "Resin-based composite - three surfaces, posterior", "Price": 280 },
{ "Procedure Code": "D2394", "Description": "Resin-based composite - four or more surfaces, posterior", "Price": 320 },
{ "Procedure Code": "D2710", "Description": "Crown - resin-based composite (indirect)", "Price": "NC" },
{ "Procedure Code": "D2740", "Description": "Crown - porcelain/ceramic", "Price": 1300 },
{ "Procedure Code": "D2750", "Description": "Crown - porcelain fused to high noble metal", "Price": 1300 },
{ "Procedure Code": "D2751", "Description": "Crown - porcelain fused to predominantly base metal", "Price": 1200 },
{ "Procedure Code": "D2752", "Description": "Crown - porcelain fused to noble metal", "Price": 1200 },
{ "Procedure Code": "D2790", "Description": "Crown - full cast high noble metal", "Price": "NC" },
{ "Procedure Code": "D2910", "Description": "Re-cement or re-bond inlay, onlay or partial coverage restoration", "Price": 57 },
{ "Procedure Code": "D2920", "Description": "Re-cement or re-bond crown", "Price": 120 },
{ "Procedure Code": "D2929", "Description": "Prefabricated porcelain/ceramic crown - primary tooth", "Price": "NC" },
{ "Procedure Code": "D2930", "Description": "Prefabricated stainless steel crown - primary tooth", "Price": "NC" },
{ "Procedure Code": "D2931", "Description": "Prefabricated stainless steel crown - permanent tooth", "Price": 171 },
{ "Procedure Code": "D2932", "Description": "Prefabricated resin crown", "Price": "NC" },
{ "Procedure Code": "D2934", "Description": "Prefabricated esthetic coated stainless steel crown - primary tooth", "Price": "NC" },
{ "Procedure Code": "D2950", "Description": "Core buildup, including any pins when required", "Price": 350 },
{ "Procedure Code": "D2951", "Description": "Pin retention - per tooth, in addition to restoration", "Price": 27 },
{ "Procedure Code": "D2954", "Description": "Prefabricated post and core in addition to crown", "Price": 450 },
{ "Procedure Code": "D2955", "Description": "Post removal", "Price": 350 },
{ "Procedure Code": "D2980", "Description": "Crown repair necessitated by restorative material failure", "Price": 115 },
{ "Procedure Code": "D2999", "Description": "Unspecified restorative procedure, by report", "Price": "IC" },
{ "Procedure Code": "D3120", "Description": "Pulp cap - indirect (excluding final restoration)", "Price": 34 },
{ "Procedure Code": "D3220", "Description": "Therapeutic pulpotomy (excluding final restoration)", "Price": "NC" },
{ "Procedure Code": "D3230", "Description": "Pulpal therapy (resorbable filling) - anterior, primary tooth", "Price": 280 },
{ "Procedure Code": "D3240", "Description": "Pulpal therapy (resorbable filling) - posterior, primary tooth", "Price": 490 },
{ "Procedure Code": "D3310", "Description": "Endodontic therapy, anterior tooth (excluding final restoration)", "Price": 900 },
{ "Procedure Code": "D3320", "Description": "Endodontic therapy, premolar tooth (excluding final restoration)", "Price": 1050 },
{ "Procedure Code": "D3330", "Description": "Endodontic therapy, molar tooth (excluding final restoration)", "Price": 1250 },
{ "Procedure Code": "D3346", "Description": "Retreatment of previous root canal therapy - anterior", "Price": 950 },
{ "Procedure Code": "D3347", "Description": "Retreatment of previous root canal therapy - premolar", "Price": 1100 },
{ "Procedure Code": "D3348", "Description": "Retreatment of previous root canal therapy - molar", "Price": 1500 },
{ "Procedure Code": "D3410", "Description": "Apicoectomy - anterior", "Price": 407 },
{ "Procedure Code": "D3421", "Description": "Apicoectomy - premolar (first root)", "Price": 460 },
{ "Procedure Code": "D3425", "Description": "Apicoectomy - molar (first root)", "Price": 598 },
{ "Procedure Code": "D3426", "Description": "Apicoectomy (each additional root)", "Price": 230 },
{ "Procedure Code": "D4210", "Description": "Gingivectomy or gingivoplasty - four or more contiguous teeth per quadrant", "Price": 307 },
{ "Procedure Code": "D4211", "Description": "Gingivectomy or gingivoplasty - one to three contiguous teeth per quadrant", "Price": 111 },
{ "Procedure Code": "D4341", "Description": "Periodontal scaling and root planing - four or more teeth per quadrant", "Price": 250 },
{ "Procedure Code": "D4342", "Description": "Periodontal scaling and root planing - one to three teeth, per quadrant", "Price": 90 },
{ "Procedure Code": "D4346", "Description": "Scaling in presence of generalized moderate or severe gingival inflammation - full mouth", "Price": 60 },
{ "Procedure Code": "D4910", "Description": "Periodontal maintenance", "Price": 250 },
{ "Procedure Code": "D5110", "Description": "Complete denture - maxillary", "Price": 1700 },
{ "Procedure Code": "D5120", "Description": "Complete denture - mandibular", "Price": 1700 },
{ "Procedure Code": "D5130", "Description": "Immediate denture - maxillary", "Price": "NC" },
{ "Procedure Code": "D5140", "Description": "Immediate denture - mandibular", "Price": "NC" },
{ "Procedure Code": "D5211", "Description": "Maxillary partial denture - resin base", "Price": 1300 },
{ "Procedure Code": "D5212", "Description": "Mandibular partial denture - resin base", "Price": 1300 },
{ "Procedure Code": "D5213", "Description": "Maxillary partial denture - cast metal framework with resin denture bases", "Price": 1700 },
{ "Procedure Code": "D5214", "Description": "Mandibular partial denture - cast metal framework with resin denture bases", "Price": 1700 },
{ "Procedure Code": "D5225", "Description": "Maxillary partial denture - flexible base", "Price": "NC" },
{ "Procedure Code": "D5226", "Description": "Mandibular partial denture - flexible base", "Price": "NC" },
{ "Procedure Code": "D5510", "Description": "Repair broken complete denture base (QUAD)", "Price": 400 },
{ "Procedure Code": "D5511", "Description": "Repair broken complete denture base, mandibular", "Price": 85 },
{ "Procedure Code": "D5512", "Description": "Repair broken complete denture base, maxillary", "Price": 85 },
{ "Procedure Code": "D5520", "Description": "Replace missing or broken teeth - complete denture (each tooth)", "Price": 200 },
{ "Procedure Code": "D5611", "Description": "Repair broken resin partial denture base, mandibular", "Price": 77 },
{ "Procedure Code": "D5612", "Description": "Repair broken resin partial denture base, maxillary", "Price": 77 },
{ "Procedure Code": "D5621", "Description": "Repair broken cast partial denture base, mandibular", "Price": 104 },
{ "Procedure Code": "D5622", "Description": "Repair broken cast partial denture base, maxillary", "Price": 104 },
{ "Procedure Code": "D5630", "Description": "Repair or replace broken retentive/clasping materials - per tooth", "Price": 99 },
{ "Procedure Code": "D5640", "Description": "Replace broken teeth - per tooth", "Price": 77 },
{ "Procedure Code": "D5650", "Description": "Add tooth to existing partial denture", "Price": 92 },
{ "Procedure Code": "D5660", "Description": "Add clasp to existing partial denture per tooth", "Price": 98 },
{ "Procedure Code": "D5730", "Description": "Reline complete maxillary denture (chairside)", "Price": 500 },
{ "Procedure Code": "D5731", "Description": "Reline complete mandibular denture (chairside)", "Price": 500 },
{ "Procedure Code": "D5740", "Description": "Reline maxillary partial denture (chairside)", "Price": "NC" },
{ "Procedure Code": "D5741", "Description": "Reline mandibular partial denture (chairside)", "Price": "NC" },
{ "Procedure Code": "D5750", "Description": "Reline complete maxillary denture (laboratory)", "Price": 600 },
{ "Procedure Code": "D5751", "Description": "Reline complete mandibular denture (laboratory)", "Price": 600 },
{ "Procedure Code": "D5760", "Description": "Reline maxillary partial denture (laboratory)", "Price": "NC" },
{ "Procedure Code": "D5761", "Description": "Reline mandibular partial denture (laboratory)", "Price": "NC" },
{ "Procedure Code": "D6010", "Description": "Surgical placement of implant body", "Price": 1600 },
{ "Procedure Code": "D6056", "Description": "Prefabricated abutment", "Price": 750 },
{ "Procedure Code": "D6057", "Description": "Custom abutment", "Price": 800 },
{ "Procedure Code": "D6058", "Description": "Implant crown - porcelain/ceramic", "Price": 1400 },
{ "Procedure Code": "D6059", "Description": "Implant crown - PFM", "Price": 1400 },
{ "Procedure Code": "D6100", "Description": "Surgical removal of implant body", "Price": 320 },
{ "Procedure Code": "D6110", "Description": "Implant/abutment supported removable denture - maxillary", "Price": 1600 },
{ "Procedure Code": "D6111", "Description": "Implant/abutment supported removable denture - mandibular", "Price": 1600 },
{ "Procedure Code": "D6241", "Description": "Pontic - porcelain fused to metal", "Price": "NC" },
{ "Procedure Code": "D6242", "Description": "Pontic - noble metal", "Price": 1400 },
{ "Procedure Code": "D6245", "Description": "Pontic - porcelain/ceramic", "Price": 1400 },
{ "Procedure Code": "D6751", "Description": "Retainer crown - porcelain fused to metal", "Price": "NC" },
{ "Procedure Code": "D6930", "Description": "Re-cement or re-bond fixed partial denture", "Price": "NC" },
{ "Procedure Code": "D6980", "Description": "Fixed partial denture repair", "Price": "NC" },
{ "Procedure Code": "D6999", "Description": "Fixed prosthodontic procedure, by report", "Price": "IC" },
{ "Procedure Code": "D7111", "Description": "Extraction, coronal remnants - primary tooth", "Price": 120 },
{ "Procedure Code": "D7140", "Description": "Extraction, erupted tooth or exposed root (simple)", "Price": 150 },
{ "Procedure Code": "D7210", "Description": "Extraction, erupted tooth requiring removal of bone and/or sectioning (surgical)", "Price": 280 },
{ "Procedure Code": "D7220", "Description": "Removal of impacted tooth - soft tissue", "Price": 380 },
{ "Procedure Code": "D7230", "Description": "Removal of impacted tooth - partially bony", "Price": 450 },
{ "Procedure Code": "D7240", "Description": "Removal of impacted tooth - completely bony", "Price": 550 },
{ "Procedure Code": "D7250", "Description": "Surgical removal of residual tooth roots", "Price": 280 },
{ "Procedure Code": "D7251", "Description": "Coronectomy - intentional partial tooth removal, impacted teeth only", "Price": 134 },
{ "Procedure Code": "D7270", "Description": "Tooth reimplantation and/or stabilization of accidentally evulsed or displaced tooth", "Price": 106 },
{ "Procedure Code": "D7280", "Description": "Surgical access of an unerupted tooth", "Price": "NC" },
{ "Procedure Code": "D7283", "Description": "Placement of device to facilitate eruption of impacted tooth", "Price": "NC" },
{ "Procedure Code": "D7310", "Description": "Alveoloplasty in conjunction with extractions - four or more teeth per quadrant", "Price": 142 },
{ "Procedure Code": "D7311", "Description": "Alveoloplasty in conjunction with extractions - one to three teeth per quadrant", "Price": 128 },
{ "Procedure Code": "D7320", "Description": "Alveoloplasty not in conjunction with extractions - four or more teeth per quadrant", "Price": 187 },
{ "Procedure Code": "D7321", "Description": "Alveoloplasty not in conjunction with extractions - one to three teeth per quadrant", "Price": 149 },
{ "Procedure Code": "D7340", "Description": "Vestibuloplasty - ridge extension (second epithelialization)", "Price": 747 },
{ "Procedure Code": "D7350", "Description": "Vestibuloplasty - ridge extension (Oral surgeon only)", "Price": 943 },
{ "Procedure Code": "D7410", "Description": "Excision of benign lesion - diameter up to 1.25 cm", "Price": 115 },
{ "Procedure Code": "D7411", "Description": "Excision of benign lesion - diameter greater than 1.25 cm", "Price": 208 },
{ "Procedure Code": "D7450", "Description": "Removal of benign odontogenic cyst or tumor - up to 1.25 cm", "Price": 248 },
{ "Procedure Code": "D7451", "Description": "Removal of benign odontogenic cyst or tumor - greater than 1.25 cm", "Price": 288 },
{ "Procedure Code": "D7460", "Description": "Removal of benign nonodontogenic cyst or tumor - up to 1.25 cm", "Price": 121 },
{ "Procedure Code": "D7461", "Description": "Removal of benign nonodontogenic cyst or tumor - greater than 1.25 cm", "Price": 143 },
{ "Procedure Code": "D7471", "Description": "Removal of lateral exostosis (maxilla or mandible)", "Price": 143 },
{ "Procedure Code": "D7472", "Description": "Removal of torus palatinus", "Price": 143 },
{ "Procedure Code": "D7473", "Description": "Removal of torus mandibularis", "Price": 143 },
{ "Procedure Code": "D7910", "Description": "Suture of recent small wounds up to 5 cm", "Price": 400 },
{ "Procedure Code": "D7950", "Description": "Bone graft - maxilla/mandible", "Price": 800 },
{ "Procedure Code": "D7953", "Description": "Bone graft for socket preservation after extraction", "Price": 800 },
{ "Procedure Code": "D7961", "Description": "Buccal/labial frenectomy (frenulectomy)", "Price": 107 },
{ "Procedure Code": "D7962", "Description": "Lingual frenectomy (frenulectomy)", "Price": 107 },
{ "Procedure Code": "D7963", "Description": "Frenuloplasty", "Price": 416 },
{ "Procedure Code": "D7970", "Description": "Excision of hyperplastic tissue - per arch", "Price": 246 },
{ "Procedure Code": "D7999", "Description": "Unspecified oral surgery procedure, by report", "Price": "IC" },
{ "Procedure Code": "D8010", "Description": "Limited orthodontic treatment of the primary dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8020", "Description": "Limited orthodontic treatment of the transitional dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8030", "Description": "Limited orthodontic treatment of the adolescent dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8040", "Description": "Limited orthodontic treatment of the adult dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8070", "Description": "Comprehensive orthodontic treatment of the transitional dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8080", "Description": "Comprehensive orthodontic treatment of the adolescent dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8090", "Description": "Comprehensive orthodontic treatment of the adult dentition (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8660", "Description": "Pre-orthodontic treatment examination (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8670", "Description": "Periodic orthodontic treatment visit (Orthodontist only)", "Price": 215 },
{ "Procedure Code": "D8680", "Description": "Orthodontic retention (Orthodontist only)", "Price": 85 },
{ "Procedure Code": "D8703", "Description": "Replacement of lost or broken retainer - maxillary (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8704", "Description": "Replacement of lost or broken retainer - mandibular (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D8999", "Description": "Unspecified orthodontic procedure, by report (Orthodontist only)", "Price": "NC" },
{ "Procedure Code": "D9110", "Description": "Palliative treatment of dental pain - per visit", "Price": 36 },
{ "Procedure Code": "D9222", "Description": "Deep sedation/general anesthesia - first 15 minutes", "Price": 90 },
{ "Procedure Code": "D9223", "Description": "Deep sedation/general anesthesia - each additional 15-minute increment", "Price": 90 },
{ "Procedure Code": "D9230", "Description": "Analgesia, anxiolysis, inhalation of nitrous oxide", "Price": 15 },
{ "Procedure Code": "D9248", "Description": "Nonintravenous conscious sedation", "Price": 45 },
{ "Procedure Code": "D9310", "Description": "Consultation - diagnostic service (Specialist only)", "Price": 63 },
{ "Procedure Code": "D9410", "Description": "House/extended care facility call, once per facility per day", "Price": 39 },
{ "Procedure Code": "D9450", "Description": "Rural add-on encounter payment", "Price": 31 },
{ "Procedure Code": "D9920", "Description": "Behavior management, by report", "Price": 86 },
{ "Procedure Code": "D9930", "Description": "Treatment of complications (postsurgical) - unusual circumstances, by report", "Price": 30 },
{ "Procedure Code": "D9941", "Description": "Fabrication of athletic mouthguard", "Price": "NC" },
{ "Procedure Code": "D9944", "Description": "Occlusal guard - hard appliance, full arch", "Price": "NC" },
{ "Procedure Code": "D9945", "Description": "Occlusal guard - soft appliance, full arch", "Price": "NC" },
{ "Procedure Code": "D9946", "Description": "Occlusal guard - hard appliance, partial arch", "Price": "NC" },
{ "Procedure Code": "D9999", "Description": "Unspecified adjunctive procedure, by report", "Price": "IC" }
]
@@ -99,6 +99,7 @@ interface ClaimFormProps {
onHandleForUnitedDHSeleniumPreAuth: (data: ClaimPreAuthData) => void;
onHandleForTuftsSCOSeleniumPreAuth: (data: ClaimPreAuthData) => void;
onHandleForDDMASeleniumClaim: (data: ClaimFormData) => void;
onHandleForMetLifeSeleniumClaim: (data: ClaimFormData) => void;
onHandleForDeltainsSeleniumClaim: (data: ClaimFormData) => void;
onHandleForUnitedDHSeleniumClaim: (data: ClaimFormData) => void;
onHandleForTuftsSCOSeleniumClaim: (data: ClaimFormData) => void;
@@ -123,6 +124,7 @@ export function ClaimForm({
onHandleForUnitedDHSeleniumPreAuth,
onHandleForTuftsSCOSeleniumPreAuth,
onHandleForDDMASeleniumClaim,
onHandleForMetLifeSeleniumClaim,
onHandleForDeltainsSeleniumClaim,
onHandleForUnitedDHSeleniumClaim,
onHandleForTuftsSCOSeleniumClaim,
@@ -1282,6 +1284,73 @@ export function ClaimForm({
onClose();
};
// MetLife Claim: saves to DB then submits via Selenium
const handleMetLifeClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
const missingFields: string[] = [];
if (!f.memberId?.trim()) missingFields.push("Member ID");
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!patient?.firstName?.trim()) missingFields.push("First Name");
if (missingFields.length > 0) {
toast({
title: "Missing Required Fields",
description: `Please fill out the following field(s): ${missingFields.join(", ")}`,
variant: "destructive",
});
return;
}
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
toast({
title: "No procedure codes",
description: "Please add at least one procedure code before submitting the claim.",
variant: "destructive",
});
return;
}
const appointmentIdToUse = appointmentId ?? null;
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
// Upload files to server so we get local filePaths for Selenium
const claimFilesMeta: ClaimFileMeta[] = mergeWithExistingAttachments(
uploadedFiles?.length ? await uploadAttachmentsToLocalFolder(uploadedFiles) : [],
);
const selectedNpiProviderId = npiProvider?.npiNumber
? npiProviders.find((p) => p.npiNumber === npiProvider.npiNumber)?.id ?? null
: null;
const createdClaim = await onSubmit({
...formToCreateClaim,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
insuranceProvider: "MetLife Dental",
appointmentId: appointmentIdToUse,
claimFiles: claimFilesMeta,
...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}),
});
onHandleForMetLifeSeleniumClaim({
...f,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
insuranceProvider: "MetLife Dental",
appointmentId: appointmentIdToUse,
insuranceSiteKey: "METLIFE",
claimId: createdClaim.id,
claimFiles: claimFilesMeta,
});
onClose();
};
// Deltains Claim: saves to DB then submits via Selenium
const handleDeltainsClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
@@ -1577,6 +1646,8 @@ export function ClaimForm({
runWithPriceCheck(handleCCAClaim);
} else if (siteKey === "DDMA") {
runWithPriceCheck(handleDDMAClaim);
} else if (siteKey === "METLIFE" || siteKey === "METLIFEDENTAL") {
runWithPriceCheck(handleMetLifeClaim);
} else if (siteKey === "DELTAINS" || siteKey === "DELTA_INS") {
runWithPriceCheck(handleDeltainsClaim);
} else if (siteKey === "TUFTSSCO" || siteKey === "TUFTS_SCO") {
@@ -2161,6 +2232,8 @@ export function ClaimForm({
handleCCAClaim(pricedForm);
} else if (key === "ddma") {
handleDDMAClaim(pricedForm);
} else if (key === "metlife" || key === "metlifedental") {
handleMetLifeClaim(pricedForm);
} else if (key === "deltains" || key === "delta_ins") {
handleDeltainsClaim(pricedForm);
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
@@ -2818,6 +2891,14 @@ export function ClaimForm({
>
Delta MA Claim
</Button>
<Button
className="w-36 bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={() => runWithPriceCheck(handleMetLifeClaim)}
disabled={!isLicensed}
title={!isLicensed ? "License required" : undefined}
>
MetLife Claim
</Button>
<Button
className="w-36 bg-indigo-600 hover:bg-indigo-700 text-white"
onClick={() => runWithPriceCheck(handleDeltainsClaim)}
@@ -2557,6 +2557,7 @@ export default function AppointmentsPage() {
onHandleForCCASeleniumPreAuth={() => {}}
onHandleForUnitedDHSeleniumPreAuth={() => {}}
onHandleForDDMASeleniumClaim={() => {}}
onHandleForMetLifeSeleniumClaim={() => {}}
onHandleForUnitedDHSeleniumClaim={() => {}}
onHandleForTuftsSCOSeleniumPreAuth={() => {}}
onHandleForTuftsSCOSeleniumClaim={() => {}}
+110
View File
@@ -76,6 +76,9 @@ export default function ClaimsPage() {
const [ddmaClaimOtpOpen, setDdmaClaimOtpOpen] = useState(false);
const [ddmaClaimOtpSubmitting, setDdmaClaimOtpSubmitting] = useState(false);
const ddmaClaimSessionIdRef = useRef<string | null>(null);
const [metlifeClaimOtpOpen, setMetlifeClaimOtpOpen] = useState(false);
const [metlifeClaimOtpSubmitting, setMetlifeClaimOtpSubmitting] = useState(false);
const metlifeClaimSessionIdRef = useRef<string | null>(null);
// Deltains claim OTP modal
const [deltainsClaimOtpOpen, setDeltainsClaimOtpOpen] = useState(false);
const [deltainsClaimOtpSubmitting, setDeltainsClaimOtpSubmitting] = useState(false);
@@ -659,6 +662,73 @@ export default function ClaimsPage() {
}
};
// MetLife claim OTP submit handler
const handleMetlifeClaimOtpSubmit = async (otp: string) => {
const sessionId = metlifeClaimSessionIdRef.current;
if (!sessionId) return;
try {
setMetlifeClaimOtpSubmitting(true);
const resp = await apiRequest("POST", "/api/claims/metlife-claim/selenium/submit-otp", {
session_id: sessionId,
otp,
socketId,
});
const data = await resp.json();
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
setMetlifeClaimOtpOpen(false);
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing MetLife claim..." }));
} catch (err: any) {
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
} finally {
setMetlifeClaimOtpSubmitting(false);
}
};
// MetLife claim selenium handler
const handleMetLifeClaimSubmitSelenium = async (data: any) => {
try {
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting MetLife claim..." }));
const response = await apiRequest("POST", "/api/claims/metlife-claim", {
data,
socketId,
});
const result = await response.json();
if (result.error) throw new Error(result.error);
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
setPendingClaimJobId(result.jobId);
// Listen for session_id so we can forward OTP if needed
const jobId = result.jobId;
const onSessionStarted = (ev: any) => {
if (String(ev?.jobId) !== String(jobId)) return;
metlifeClaimSessionIdRef.current = ev.session_id ?? null;
};
const onOtpRequired = (ev: any) => {
if (String(ev?.jobId) !== String(jobId)) return;
if (ev.session_id) metlifeClaimSessionIdRef.current = ev.session_id;
setMetlifeClaimOtpOpen(true);
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for MetLife. Please enter the code." }));
};
const onDone = (ev: any) => {
if (String(ev?.jobId) !== String(jobId)) return;
socket.off("selenium:metlife_claim_started", onSessionStarted);
socket.off("selenium:otp_required", onOtpRequired);
socket.off("job:update", onDone);
setMetlifeClaimOtpOpen(false);
metlifeClaimSessionIdRef.current = null;
};
socket.on("selenium:metlife_claim_started", onSessionStarted);
socket.on("selenium:otp_required", onOtpRequired);
socket.on("job:update", onDone);
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "MetLife claim queued. Awaiting Selenium..." }));
toast({ title: "MetLife Claim queued", description: "Selenium is logging in to the MetLife portal.", variant: "default" });
} catch (error: any) {
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "MetLife claim failed" }));
toast({ title: "MetLife Claim error", description: error.message || "An error occurred.", variant: "destructive" });
}
};
// Deltains claim OTP submit handler
const handleDeltainsClaimOtpSubmit = async (otp: string) => {
const sessionId = deltainsClaimSessionIdRef.current;
@@ -1450,6 +1520,7 @@ export default function ClaimsPage() {
onHandleForCCASeleniumPreAuth={handleCCAPreAuthSubmitSelenium}
onHandleForUnitedDHSeleniumPreAuth={handleUnitedDHPreAuthSubmitSelenium}
onHandleForDDMASeleniumClaim={handleDDMAClaimSubmitSelenium}
onHandleForMetLifeSeleniumClaim={handleMetLifeClaimSubmitSelenium}
onHandleForDeltainsSeleniumClaim={handleDeltainsClaimSubmitSelenium}
onHandleForUnitedDHSeleniumClaim={handleUnitedDHClaimSubmitSelenium}
onHandleForTuftsSCOSeleniumPreAuth={handleTuftsSCOPreAuthSubmitSelenium}
@@ -1510,6 +1581,45 @@ export default function ClaimsPage() {
</div>
)}
{/* MetLife Claim OTP Modal */}
{metlifeClaimOtpOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Enter OTP — MetLife Claim</h2>
<button type="button" onClick={() => setMetlifeClaimOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
</div>
<p className="text-sm text-slate-500 mb-4">
MetLife emailed a verification code. Enter it here, or type it straight into the browser window.
</p>
<form onSubmit={(e) => {
e.preventDefault();
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
if (input?.value.trim()) handleMetlifeClaimOtpSubmit(input.value.trim());
}} className="space-y-4">
<div className="space-y-2">
<label htmlFor="metlife-claim-otp" className="text-sm font-medium">OTP</label>
<input
id="metlife-claim-otp"
name="otp"
placeholder="Enter OTP code"
autoFocus
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div className="flex justify-end gap-3">
<button type="button" onClick={() => setMetlifeClaimOtpOpen(false)} disabled={metlifeClaimOtpSubmitting}
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
<button type="submit" disabled={metlifeClaimOtpSubmitting}
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
{metlifeClaimOtpSubmitting ? "Submitting..." : "Submit OTP"}
</button>
</div>
</form>
</div>
</div>
)}
{/* Deltains Claim OTP Modal */}
{deltainsClaimOtpOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
@@ -9,6 +9,7 @@ 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 ----------------------------- */
@@ -29,6 +30,7 @@ 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
@@ -98,6 +100,15 @@ const ALTUS_CODE_MAP: Map<string, CodeRow> = (() => {
return m;
})();
const METLIFE_CODE_MAP: Map<string, CodeRow> = (() => {
const m = new Map<string, CodeRow>();
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<string, CodeRow> = (() => {
const m = new Map<string, CodeRow>();
for (const r of UNITEDDH_CODE_TABLE) {
@@ -141,6 +152,7 @@ function getCodeMap(insuranceSiteKey?: string): Map<string, CodeRow> {
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;
@@ -422,7 +434,7 @@ export function findPriceMismatches(
patientDOB: string,
serviceDate: string,
): PriceMismatch[] {
const supported = ["MH", "MASSHEALTH", "CCA", "DDMA", "UNITEDDH", "UNITEDSCO", "UNITED_UCARD", "UNITEDUCARD", "TUFTSSCO"];
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);