Skip to content

Commit b65e3b3

Browse files
committed
#128 differentiate arrest and citation dates in case summaries
1 parent 4dd324b commit b65e3b3

6 files changed

Lines changed: 91 additions & 21 deletions

File tree

frontend/src/components/app/SearchResult.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,16 @@ const SearchResult: React.FC<SearchResultProps> = ({ searchResult: sr }) => {
6868
(() => {
6969
const d = new Date(summary.arrestOrCitationDate);
7070
if (!isNaN(d.getTime())) {
71+
const label =
72+
summary.arrestOrCitationType === 'Arrest'
73+
? 'Arrest Date:'
74+
: summary.arrestOrCitationType === 'Citation'
75+
? 'Citation Date:'
76+
: 'Arrest/Citation Date:';
77+
7178
return (
7279
<p className="mt-1 text-sm text-gray-600">
73-
<span className="font-medium">Arrest/Citation Date:</span> {d.toLocaleDateString()}
80+
<span className="font-medium">{label}</span> {d.toLocaleDateString()}
7481
</p>
7582
);
7683
}

frontend/src/components/app/__tests__/SearchResult.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,15 @@ describe('SearchResult component', () => {
176176
caseName: 'State vs. Doe',
177177
court: 'Circuit Court',
178178
arrestOrCitationDate: '2022-02-15T00:00:00Z',
179+
arrestOrCitationType: 'Arrest',
179180
charges: [],
180181
},
181182
});
182183

183184
render(<SearchResult searchResult={testCase} />);
184185

185-
// Label should be present
186-
expect(screen.getByText(/Arrest\/Citation Date:/)).toBeInTheDocument();
186+
// Label should be present and explicitly show 'Arrest Date'
187+
expect(screen.getByText(/Arrest Date:/)).toBeInTheDocument();
187188

188189
// The displayed date should contain the year 2022 (locale independent check)
189190
expect(screen.getByText(/2022/)).toBeInTheDocument();

serverless/lib/CaseProcessor.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -745,28 +745,54 @@ function buildCaseSummary(rawData: Record<string, PortalApiResponse>): CaseSumma
745745
});
746746
});
747747

748-
// Process case-level events to determine arrest or citation date (LPSD events)
748+
// Process case-level events to determine arrest or citation date (LPSD -> Arrest, CIT -> Citation)
749749
try {
750750
const caseEvents = rawData['caseEvents']?.['Events'] || [];
751751
console.log(`📋 Found ${caseEvents.length} case events`);
752752

753-
// Filter only events that have the LPSD TypeId and a valid EventDate
754-
const lpsdEvents = caseEvents.filter(
755-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
756-
(ev: any) =>
757-
ev && ev['Event'] && ev['Event']['TypeId'] && ev['Event']['TypeId']['Word'] === 'LPSD' && ev['Event']['EventDate']
753+
// Filter only events that have the LPSD (arrest) or CIT (citation) TypeId and a valid EventDate
754+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
755+
const candidateEvents = caseEvents.filter(
756+
(ev: any) => ev && ev['Event'] && ev['Event']['TypeId'] && ev['Event']['TypeId']['Word'] && ev['Event']['EventDate']
758757
);
759758

760-
if (lpsdEvents.length > 0) {
761-
// Parse dates and find the earliest
762-
const parsedDates: Date[] = lpsdEvents
763-
.map((ev: any) => parseMMddyyyyToDate(ev['Event']['EventDate']))
764-
.filter((d: Date | null): d is Date => d !== null);
759+
console.log(`🔎 Found ${candidateEvents.length} candidate events for arrest/citation`);
760+
761+
if (candidateEvents.length > 0) {
762+
const parsedCandidates: { date: Date; type: 'Arrest' | 'Citation'; raw: string }[] = [];
763+
764+
candidateEvents.forEach((ev: any, idx: number) => {
765+
const typeWord = ev['Event']['TypeId']['Word'];
766+
const eventDateStr = ev['Event']['EventDate'];
767+
768+
if (typeWord !== 'LPSD' && typeWord !== 'CIT') {
769+
return;
770+
}
771+
772+
const parsed = parseMMddyyyyToDate(eventDateStr);
773+
if (parsed) {
774+
parsedCandidates.push({
775+
date: parsed,
776+
type: typeWord === 'LPSD' ? 'Arrest' : 'Citation',
777+
raw: eventDateStr,
778+
});
779+
console.log(` ✔ Candidate #${idx}: Type=${typeWord}, Parsed=${parsed.toISOString()}`);
780+
} else {
781+
console.warn(` ✖ Candidate #${idx} has unparseable date: ${eventDateStr}`);
782+
}
783+
});
765784

766-
if (parsedDates.length > 0) {
767-
const earliest = parsedDates.reduce((min, d) => (d.getTime() < min.getTime() ? d : min), parsedDates[0]);
768-
caseSummary.arrestOrCitationDate = earliest.toISOString();
769-
console.log(`🔔 Set arrestOrCitationDate to ${caseSummary.arrestOrCitationDate}`);
785+
if (parsedCandidates.length > 0) {
786+
// Choose the earliest date among all matching candidates
787+
const earliest = parsedCandidates.reduce(
788+
(min, c) => (c.date.getTime() < min.date.getTime() ? c : min),
789+
parsedCandidates[0]
790+
);
791+
caseSummary.arrestOrCitationDate = earliest.date.toISOString();
792+
caseSummary.arrestOrCitationType = earliest.type;
793+
console.log(`🔔 Set ${earliest.type} date to ${caseSummary.arrestOrCitationDate}`);
794+
} else {
795+
console.log('No parsable arrest/citation dates found among candidates');
770796
}
771797
}
772798
} catch (evtErr) {

serverless/lib/__tests__/caseProcessor.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('CaseProcessor', () => {
145145
describe('buildCaseSummary', () => {
146146
const { buildCaseSummary } = CaseProcessor as any;
147147

148-
it('extracts the earliest LPSD Event.EventDate and sets arrestOrCitationDate as ISO string', () => {
148+
it('extracts the earliest LPSD Event.EventDate and sets arrestOrCitationDate and type as Arrest', () => {
149149
const rawData = {
150150
summary: {
151151
CaseSummaryHeader: {
@@ -186,13 +186,43 @@ describe('CaseProcessor', () => {
186186

187187
expect(summary).not.toBeNull();
188188
expect(summary?.arrestOrCitationDate).toBeDefined();
189+
expect(summary?.arrestOrCitationType).toBe('Arrest');
189190

190191
// Expected earliest LPSD date is 02/10/2021 -> construct UTC Date and compare ISO
191192
const expectedIso = new Date(Date.UTC(2021, 1, 10)).toISOString();
192193
expect(summary?.arrestOrCitationDate).toBe(expectedIso);
193194
});
194195

195-
it('does not set arrestOrCitationDate when no LPSD events present', () => {
196+
it('selects CIT over LPSD if earlier (sets type Citation)', () => {
197+
const rawData = {
198+
summary: {
199+
CaseSummaryHeader: {
200+
Style: 'State vs. Someone',
201+
Heading: 'Circuit Court',
202+
CaseId: 'case-234',
203+
},
204+
},
205+
charges: { Charges: [] },
206+
dispositionEvents: { Events: [] },
207+
caseEvents: {
208+
Events: [
209+
{ Event: { TypeId: { Word: 'LPSD' }, EventDate: '03/15/2021' } },
210+
{ Event: { TypeId: { Word: 'CIT' }, EventDate: '02/09/2021' } },
211+
],
212+
},
213+
};
214+
215+
const summary = buildCaseSummary(rawData);
216+
217+
expect(summary).not.toBeNull();
218+
expect(summary?.arrestOrCitationDate).toBeDefined();
219+
expect(summary?.arrestOrCitationType).toBe('Citation');
220+
221+
const expectedIso = new Date(Date.UTC(2021, 1, 9)).toISOString();
222+
expect(summary?.arrestOrCitationDate).toBe(expectedIso);
223+
});
224+
225+
it('does not set arrestOrCitationDate when no LPSD/CIT events present', () => {
196226
const rawData = {
197227
summary: {
198228
CaseSummaryHeader: {
@@ -216,6 +246,7 @@ describe('CaseProcessor', () => {
216246

217247
expect(summary).not.toBeNull();
218248
expect(summary?.arrestOrCitationDate).toBeUndefined();
249+
expect(summary?.arrestOrCitationType).toBeUndefined();
219250
});
220251

221252
it('ignores malformed LPSD Event.EventDate values', () => {
@@ -243,6 +274,7 @@ describe('CaseProcessor', () => {
243274
const summary = buildCaseSummary(rawData);
244275
expect(summary).not.toBeNull();
245276
expect(summary?.arrestOrCitationDate).toBeUndefined();
277+
expect(summary?.arrestOrCitationType).toBeUndefined();
246278
});
247279
});
248280
});

serverless/lib/__tests__/storageClient.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ describe('StorageClient.getSearchResults resilience', () => {
422422
expect(mockSetImmediate).toHaveBeenCalled();
423423
});
424424

425-
it('should preserve arrestOrCitationDate when present in summary', async () => {
425+
it('should preserve arrestOrCitationDate and type when present in summary', async () => {
426426
const { validateAndProcessCaseSummary } = require('../StorageClient');
427427

428428
const caseNumber = 'ARRESTDATE001';
@@ -438,6 +438,7 @@ describe('StorageClient.getSearchResults resilience', () => {
438438
court: 'Test Court',
439439
charges: [],
440440
arrestOrCitationDate: '2021-02-10T00:00:00.000Z',
441+
arrestOrCitationType: 'Arrest',
441442
};
442443

443444
const result = await validateAndProcessCaseSummary(caseNumber, caseData, validSummaryItem);

shared/types/ZipCase.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ export interface Charge {
2828
dispositions: Disposition[];
2929
}
3030

31+
export type ArrestOrCitationType = 'Arrest' | 'Citation';
32+
3133
export interface CaseSummary {
3234
caseName: string;
3335
court: string;
3436
charges: Charge[];
3537
arrestOrCitationDate?: string;
38+
arrestOrCitationType?: ArrestOrCitationType;
3639
}
3740

3841
export interface ZipCase {

0 commit comments

Comments
 (0)