@@ -502,8 +502,32 @@ const caseEndpoints: Record<string, EndpointConfig> = {
502502 financialSummary : {
503503 path : "Service/FinancialSummary('{caseId}')" ,
504504 } ,
505+ caseEvents : {
506+ path : "Service/CaseEvents('{caseId}')?top=200" ,
507+ } ,
505508} ;
506509
510+ function parseMMddyyyyToDate ( dateStr : string ) : Date | null {
511+ if ( ! dateStr || typeof dateStr !== 'string' ) {
512+ return null ;
513+ }
514+
515+ const parts = dateStr . split ( '/' ) ;
516+ if ( parts . length !== 3 ) {
517+ return null ;
518+ }
519+
520+ const month = parseInt ( parts [ 0 ] , 10 ) ;
521+ const day = parseInt ( parts [ 1 ] , 10 ) ;
522+ const year = parseInt ( parts [ 2 ] , 10 ) ;
523+
524+ if ( Number . isNaN ( month ) || Number . isNaN ( day ) || Number . isNaN ( year ) ) {
525+ return null ;
526+ }
527+
528+ return new Date ( Date . UTC ( year , month - 1 , day ) ) ;
529+ }
530+
507531async function fetchCaseSummary ( caseId : string ) : Promise < CaseSummary | null > {
508532 try {
509533 const portalCaseUrl = process . env . PORTAL_CASE_URL ;
@@ -578,8 +602,8 @@ async function fetchCaseSummary(caseId: string): Promise<CaseSummary | null> {
578602 // Wait for all promises to resolve
579603 const results = await Promise . all ( endpointPromises ) ;
580604
581- // Check if any endpoint failed
582- const requiredFailure = results . find ( result => ! result . success ) ;
605+ // Treat caseEvents as optional; if any other endpoint failed, consider it a required failure
606+ const requiredFailure = results . find ( result => ! result . success && result . key !== 'caseEvents' ) ;
583607
584608 if ( requiredFailure ) {
585609 console . error ( `Required endpoint ${ requiredFailure . key } failed: ${ requiredFailure . error } ` ) ;
@@ -615,6 +639,7 @@ function buildCaseSummary(rawData: Record<string, PortalApiResponse>): CaseSumma
615639 caseName : rawData [ 'summary' ] [ 'CaseSummaryHeader' ] [ 'Style' ] || '' ,
616640 court : rawData [ 'summary' ] [ 'CaseSummaryHeader' ] [ 'Heading' ] || '' ,
617641 charges : [ ] ,
642+ filingAgency : null ,
618643 } ;
619644
620645 const chargeMap = new Map < number , Charge > ( ) ;
@@ -639,8 +664,21 @@ function buildCaseSummary(rawData: Record<string, PortalApiResponse>): CaseSumma
639664 } ,
640665 fine : typeof chargeOffense [ 'FineAmount' ] === 'number' ? chargeOffense [ 'FineAmount' ] : 0 ,
641666 dispositions : [ ] ,
667+ filingAgency : null ,
668+ filingAgencyAddress : [ ] ,
642669 } ;
643670
671+ const filingAgencyRaw = chargeData [ 'FilingAgencyDescription' ] ;
672+ if ( filingAgencyRaw ) {
673+ charge . filingAgency = String ( filingAgencyRaw ) . trim ( ) ;
674+ }
675+
676+ // Extract filing agency address if present. It will be an array of strings.
677+ const filingAgencyAddressRaw = chargeData [ 'FilingAgencyAddress' ] ;
678+ if ( filingAgencyAddressRaw ) {
679+ charge . filingAgencyAddress . push ( ...( filingAgencyAddressRaw as any ) ) ;
680+ }
681+
644682 // Add to charges array
645683 caseSummary . charges . push ( charge ) ;
646684
@@ -650,11 +688,27 @@ function buildCaseSummary(rawData: Record<string, PortalApiResponse>): CaseSumma
650688 }
651689 } ) ;
652690
691+ // After processing charges, derive top-level filing agency if appropriate
692+ try {
693+ const definedAgencies = caseSummary . charges . map ( ch => ch . filingAgency ) . filter ( ( a ) : a is string => a !== null && a . length > 0 ) ;
694+
695+ const uniqueAgencies = Array . from ( new Set ( definedAgencies ) ) ;
696+
697+ // If there's at least one defined agency, and all defined agencies are identical,
698+ // set it on the case summary. Charges that lack an agency (null) are ignored for this decision.
699+ if ( uniqueAgencies . length === 1 && uniqueAgencies [ 0 ] ) {
700+ caseSummary . filingAgency = uniqueAgencies [ 0 ] ;
701+ console . log ( `🔔 Set Filing Agency to ${ caseSummary . filingAgency } ` ) ;
702+ }
703+ } catch ( faErr ) {
704+ console . error ( 'Error computing top-level filing agency:' , faErr ) ;
705+ }
706+
653707 // Process dispositions and link them to charges
654- const events = rawData [ 'dispositionEvents' ] [ 'Events' ] || [ ] ;
655- console . log ( `📋 Found ${ events . length } disposition events` ) ;
708+ const dispositionEvents = rawData [ 'dispositionEvents' ] [ 'Events' ] || [ ] ;
709+ console . log ( `📋 Found ${ dispositionEvents . length } disposition events` ) ;
656710
657- events
711+ dispositionEvents
658712 . filter (
659713 // eslint-disable-next-line @typescript-eslint/no-explicit-any
660714 ( eventData : any ) => eventData && eventData [ 'Type' ] === 'CriminalDispositionEvent'
@@ -721,6 +775,60 @@ function buildCaseSummary(rawData: Record<string, PortalApiResponse>): CaseSumma
721775 } ) ;
722776 } ) ;
723777
778+ // Process case-level events to determine arrest or citation date (LPSD -> Arrest, CIT -> Citation)
779+ try {
780+ const caseEvents = rawData [ 'caseEvents' ] ?. [ 'Events' ] || [ ] ;
781+ console . log ( `📋 Found ${ caseEvents . length } case events` ) ;
782+
783+ // Filter only events that have the LPSD (arrest) or CIT (citation) TypeId and a valid EventDate
784+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
785+ const candidateEvents = caseEvents . filter (
786+ ( ev : any ) => ev && ev [ 'Event' ] && ev [ 'Event' ] [ 'TypeId' ] && ev [ 'Event' ] [ 'TypeId' ] [ 'Word' ] && ev [ 'Event' ] [ 'EventDate' ]
787+ ) ;
788+
789+ console . log ( `🔎 Found ${ candidateEvents . length } candidate events for arrest/citation` ) ;
790+
791+ if ( candidateEvents . length > 0 ) {
792+ const parsedCandidates : { date : Date ; type : 'Arrest' | 'Citation' ; raw : string } [ ] = [ ] ;
793+
794+ candidateEvents . forEach ( ( ev : any , idx : number ) => {
795+ const typeWord = ev [ 'Event' ] [ 'TypeId' ] [ 'Word' ] ;
796+ const eventDateStr = ev [ 'Event' ] [ 'EventDate' ] ;
797+
798+ if ( typeWord !== 'LPSD' && typeWord !== 'CIT' ) {
799+ return ;
800+ }
801+
802+ const parsed = parseMMddyyyyToDate ( eventDateStr ) ;
803+ if ( parsed ) {
804+ parsedCandidates . push ( {
805+ date : parsed ,
806+ type : typeWord === 'LPSD' ? 'Arrest' : 'Citation' ,
807+ raw : eventDateStr ,
808+ } ) ;
809+ console . log ( ` ✔ Candidate #${ idx } : Type=${ typeWord } , Parsed=${ parsed . toISOString ( ) } ` ) ;
810+ } else {
811+ console . warn ( ` ✖ Candidate #${ idx } has unparseable date: ${ eventDateStr } ` ) ;
812+ }
813+ } ) ;
814+
815+ if ( parsedCandidates . length > 0 ) {
816+ // Choose the earliest date among all matching candidates
817+ const earliest = parsedCandidates . reduce (
818+ ( min , c ) => ( c . date . getTime ( ) < min . date . getTime ( ) ? c : min ) ,
819+ parsedCandidates [ 0 ]
820+ ) ;
821+ caseSummary . arrestOrCitationDate = earliest . date . toISOString ( ) ;
822+ caseSummary . arrestOrCitationType = earliest . type ;
823+ console . log ( `🔔 Set ${ earliest . type } date to ${ caseSummary . arrestOrCitationDate } ` ) ;
824+ } else {
825+ console . log ( 'No parsable arrest/citation dates found among candidates' ) ;
826+ }
827+ }
828+ } catch ( evtErr ) {
829+ console . error ( 'Error processing caseEvents for arrest/citation date:' , evtErr ) ;
830+ }
831+
724832 return caseSummary ;
725833 } catch ( error ) {
726834 AlertService . logError ( Severity . ERROR , AlertCategory . SYSTEM , 'Error building case summary from raw data' , error as Error , {
@@ -736,6 +844,7 @@ const CaseProcessor = {
736844 processCaseData,
737845 queueCasesForSearch,
738846 fetchCaseIdFromPortal,
847+ buildCaseSummary,
739848} ;
740849
741850export default CaseProcessor ;
0 commit comments