@@ -136,18 +136,35 @@ function calcAge(dob: string | null | undefined, on?: string | null): number | n
} catch { return null ; }
}
function checkD1120Age (
matched : CdtResult [ ] ,
/**
* The patient's actual age is authoritative over whatever the user typed in notes
* ("adult prophy" / "child prophy") — D1110 is for 14+, D1120 is for under 14.
* Silently rewrites the matched code/description to match age and returns an
* informational note when a correction was made, so the user is told why what
* they typed differs from what's being claimed.
*/
function correctProphyForAge (
results : CdtResult [ ] ,
patientName : string ,
dob : string | null | undefined ,
serviceDate? : string | null
) : ChatResponse | null {
if ( ! matched . some ( ( r ) = > r . code === "D1120" ) ) return null ;
) : { results : CdtResult [ ] ; note : string | null } {
const age = calcAge ( dob , serviceDate ) ;
if ( age === null || age < 14 ) return null ;
return {
reply : ` ${ patientName } is ${ age } years old. D1120 (child prophy) is only for patients under 14 — I recommend using D1110 (adult prophy) instead. If you still need D1120, please claim manually for this patient. ` ,
} ;
if ( age === null ) return { results , note : null } ;
let note : string | null = null ;
const corrected = results . map ( ( r ) = > {
if ( r . code === "D1110" && age < 14 ) {
note = ` Note: ${ patientName } is ${ age } , so I used D1120 (child prophy) instead of D1110 — age overrides what was written. ` ;
return { . . . r , code : "D1120" , description : "Prophylaxis – child, 0-13 yo" } ;
}
if ( r . code === "D1120" && age >= 14 ) {
note = ` Note: ${ patientName } is ${ age } , so I used D1110 (adult prophy) instead of D1120 — age overrides what was written. ` ;
return { . . . r , code : "D1110" , description : "Prophylaxis – adult, 14 yo or older" } ;
}
return r ;
} ) ;
return { results : corrected , note } ;
}
function patientToResult ( p : any ) : ResolvedPatient {
@@ -703,7 +720,9 @@ async function handleBatchCheckAndClaim(
siteKey : string ;
autoCheck : string ;
patient : ResolvedPatient | null ;
matchedCodes : CdtResult [ ] ;
} [ ] = [ ] ;
const ageNotes : string [ ] = [ ] ;
for ( const { memberId , dob } of pairs ) {
const id = memberId ? . trim ( ) ;
@@ -720,12 +739,19 @@ async function handleBatchCheckAndClaim(
c . insuranceHint ? ? null
) ? ? "MH" ;
const label = patient
? ` ${ patient . firstName ? ? "" } ${ patient . lastName ? ? "" } ` . trim ( )
: ` ID ${ id } ` ;
const { results : patientMatched , note } = correctProphyForAge ( matched , label , resolvedDob , c . appointmentDate ) ;
if ( note ) ageNotes . push ( note ) ;
resolved . push ( {
memberId : id ,
dob : resolvedDob ,
siteKey ,
autoCheck : siteKeyToAutoCheck ( siteKey , resolvedDob ) ,
patient ,
matchedCodes : patientMatched ,
} ) ;
}
@@ -741,11 +767,23 @@ async function handleBatchCheckAndClaim(
} ) ;
const codeList = matched . map ( ( r ) = > ` ${ r . code } ( ${ r . description } ) ` ) . join ( ", " ) ;
let reply = ` Ready to check eligibility and claim ${ codeList } for ${ resolved . length } patients: ${ labels . join ( ", " ) } . ` ;
if ( ageNotes . length > 0 ) {
reply += ` ${ ageNotes . join ( " " ) } ` ;
}
return {
reply : ` Ready to check eligibility and claim ${ codeList } for ${ resolved . length } patients: ${ labels . join ( ", " ) } . ` ,
reply ,
action : "batch_check_and_claim_ready" ,
actionData : {
queue : resolved ,
queue : resolved.map ( ( r ) = > ( {
memberId : r.memberId ,
dob : r.dob ,
siteKey : r.siteKey ,
autoCheck : r.autoCheck ,
patient : r.patient ,
matchedCodes : r.matchedCodes.map ( ( m ) = > ( { code : m.code ! , description : m.description , toothNumber : m.toothNumber , toothSurface : m.toothSurface , quad : m.quad } ) ) ,
} ) ) ,
matchedCodes : matched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
renderingProvider : c.renderingProvider ? ? null ,
} ,
@@ -794,8 +832,10 @@ async function handleBatchClaim(
siteKey : string ;
serviceDate : string | null ;
appointmentId : number | null ;
matchedCodes : CdtResult [ ] ;
} [ ] = [ ] ;
const notFound : string [ ] = [ ] ;
const ageNotes : string [ ] = [ ] ;
for ( const name of names ) {
const trimmed = name . trim ( ) ;
@@ -808,8 +848,8 @@ async function handleBatchClaim(
const patient = patientToResult ( raw ) ;
const fullName = ` ${ patient . firstName ? ? "" } ${ patient . lastName ? ? "" } ` . trim ( ) ;
const d1120Warning = checkD1120Age ( matched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
if ( d1120Warning ) return d1120Warning ;
const { results : patientMatched , note } = correctProphyForAge ( matched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
if ( note ) ageNotes . push ( note ) ;
const siteKey = resolveSiteKey ( patient . insuranceProvider ? ? null , c . insuranceHint ? ? null ) ? ? "MH" ;
@@ -820,7 +860,7 @@ async function handleBatchClaim(
serviceDate = ` ${ now . getFullYear ( ) } - ${ String ( now . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) } - ${ String ( now . getDate ( ) ) . padStart ( 2 , "0" ) } ` ;
}
resolved . push ( { patient , siteKey , serviceDate , appointmentId } ) ;
resolved . push ( { patient , siteKey , serviceDate , appointmentId , matchedCodes : patientMatched } ) ;
}
if ( notFound . length > 0 && resolved . length === 0 ) {
@@ -835,6 +875,9 @@ async function handleBatchClaim(
if ( notFound . length > 0 ) {
reply += ` Could not find: ${ notFound . join ( ", " ) } . ` ;
}
if ( ageNotes . length > 0 ) {
reply += ` ${ ageNotes . join ( " " ) } ` ;
}
return {
reply ,
@@ -845,6 +888,7 @@ async function handleBatchClaim(
siteKey : r.siteKey ,
serviceDate : r.serviceDate ,
appointmentId : r.appointmentId ,
matchedCodes : r.matchedCodes.map ( ( m ) = > ( { code : m.code ! , description : m.description , toothNumber : m.toothNumber , toothSurface : m.toothSurface , quad : m.quad } ) ) ,
} ) ) ,
matchedCodes : matched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
renderingProvider : c.renderingProvider ? ? null ,
@@ -879,6 +923,7 @@ async function handleMultiClaim(
matchedCodes : { code : string ; description : string ; toothNumber? : string ; toothSurface? : string ; quad? : string } [ ] ;
} [ ] = [ ] ;
const notFound : string [ ] = [ ] ;
const ageNotes : string [ ] = [ ] ;
for ( const group of groups ) {
const name = group . patientName ? . trim ( ) ;
@@ -908,8 +953,8 @@ async function handleMultiClaim(
const patient = patientToResult ( raw ) ;
const fullName = ` ${ patient . firstName ? ? "" } ${ patient . lastName ? ? "" } ` . trim ( ) ;
const d1120Warning = checkD1120Age ( matched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
if ( d1120Warning ) return d1120Warning ;
const { results : correctedMatched , note } = correctProphyForAge ( matched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
if ( note ) ageNotes . push ( note ) ;
const siteKey = resolveSiteKey ( patient . insuranceProvider ? ? null , c . insuranceHint ? ? null ) ? ? "MH" ;
@@ -924,7 +969,7 @@ async function handleMultiClaim(
siteKey ,
serviceDate ,
appointmentId : null ,
matchedCodes : matched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
matchedCodes : correctedMatched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
} ) ;
}
@@ -941,6 +986,9 @@ async function handleMultiClaim(
if ( notFound . length > 0 ) {
reply += ` Could not find: ${ notFound . join ( ", " ) } . ` ;
}
if ( ageNotes . length > 0 ) {
reply += ` ${ ageNotes . join ( " " ) } ` ;
}
return {
reply ,
@@ -1049,13 +1097,13 @@ async function handleCheckAndClaim(
? ` ${ patient . firstName ? ? "" } ${ patient . lastName ? ? "" } ` . trim ( )
: ` Member ID ${ memberId } ` ;
// D1120 age check — recommend D1110 for patients 14+
const d1120Warning = checkD1120Age ( matched , label , patient ? . dateOfBirth ? ? resolvedDob , c . appointmentDate ) ;
if ( d1120Warning ) return d1120Warning ;
// Patient's age is authoritative over notes text for prophy code (D1110 vs D1120)
const { results : correctedMatched , note : ageNote } = correctProphyForAge ( matched , label , patient ? . dateOfBirth ? ? resolvedDob , c . appointmentDate ) ;
const reply = ` Ready to check eligibility for ${ label } and claim: ${
matched . map ( ( r ) = > ` ${ r . code } ( ${ r . description } ) ` ) . join ( ", " ) || "no procedures mapped"
let reply = ` Ready to check eligibility for ${ label } and claim: ${
correctedMatched . map ( ( r ) = > ` ${ r . code } ( ${ r . description } ) ` ) . join ( ", " ) || "no procedures mapped"
} . ` ;
if ( ageNote ) reply += ` ${ ageNote } ` ;
return {
reply ,
@@ -1067,7 +1115,7 @@ async function handleCheckAndClaim(
siteKey ,
autoCheck : siteKeyToAutoCheck ( siteKey ) ,
cdtResults ,
matchedCodes : matched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
matchedCodes : correctedMatched.map ( ( r ) = > ( { code : r.code ! , description : r.description , toothNumber : r.toothNumber , toothSurface : r.toothSurface , quad : r.quad } ) ) ,
serviceDate : c.appointmentDate ? ? null ,
renderingProvider : c.renderingProvider ? ? null ,
} ,
@@ -1107,7 +1155,7 @@ async function handleClaimOnly(
}
const cdtResults = lookupCdtCodes ( procedureNames , customAliases ) ;
const matched = cdtResults . filter ( ( r ) = > r . code !== null ) ;
const rawMatched = cdtResults . filter ( ( r ) = > r . code !== null ) ;
const unmatched = cdtResults . filter ( ( r ) = > r . code === null ) ;
// Block if any term couldn't be mapped — ask instead of proceeding
@@ -1116,13 +1164,12 @@ async function handleClaimOnly(
return {
reply : ` I don't recognize ${ phrases . map ( ( p ) = > ` " ${ p } " ` ) . join ( ", " ) } . What CDT code ${ phrases . length > 1 ? "s are" : " is" } ${ phrases . length > 1 ? "they" : "it" } ? (e.g. D0272) ` ,
action : "need_cdt_clarification" ,
actionData : { unknownPhrases : phrases , matchedSoFar : matched.map ( ( r ) = > ( { code : r.code ! , description : r.description } ) ) } ,
actionData : { unknownPhrases : phrases , matchedSoFar : rawMatched.map ( ( r ) = > ( { code : r.code ! , description : r.description } ) ) } ,
} ;
}
// D1120 age check — recommend D1110 for patients 14+
const d1120Warning = checkD1120Age ( matched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
if ( d1120Warning ) return d1120Warning ;
// Patient's age is authoritative over notes text for prophy code (D1110 vs D1120)
const { results : matched , note : ageNote } = correctProphyForAge ( rawMatched , fullName , patient . dateOfBirth , c . appointmentDate ) ;
// Resolve service date: use explicit date from message, then latest appointment, then ask
let serviceDate : string | null = c . appointmentDate ? ? null ;
@@ -1180,6 +1227,7 @@ async function handleClaimOnly(
if ( unmatched . length > 0 ) {
reply += ` Could not map: ${ unmatched . map ( ( r ) = > ` " ${ r . input } " ` ) . join ( ", " ) } — verify manually. ` ;
}
if ( ageNote ) reply += ` ${ ageNote } ` ;
return {
reply ,