diff --git a/frontend/src/components/app/SearchResult.tsx b/frontend/src/components/app/SearchResult.tsx index 5001ced..7e6b83b 100644 --- a/frontend/src/components/app/SearchResult.tsx +++ b/frontend/src/components/app/SearchResult.tsx @@ -64,6 +64,32 @@ const SearchResult: React.FC = ({ searchResult: sr }) => {

{summary.caseName}

{summary.court}

+ {summary.arrestOrCitationDate && + (() => { + const d = new Date(summary.arrestOrCitationDate); + if (!isNaN(d.getTime())) { + const label = + summary.arrestOrCitationType === 'Arrest' + ? 'Arrest Date:' + : summary.arrestOrCitationType === 'Citation' + ? 'Citation Date:' + : 'Arrest/Citation Date:'; + + return ( +

+ {label} {d.toLocaleDateString()} +

+ ); + } + return null; + })()} + + {/* Filing agency: shown at top-level if the case summary has a single filing agency for all charges */} + {summary.filingAgency && ( +

+ Filing Agency: {summary.filingAgency} +

+ )}
{summary.charges && summary.charges.length > 0 && ( @@ -76,11 +102,17 @@ const SearchResult: React.FC = ({ searchResult: sr }) => {
Filed:{' '} - {new Date(charge.filedDate).toLocaleDateString()} + {(() => { + const d = new Date(charge.filedDate); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString(); + })()}
Offense:{' '} - {new Date(charge.offenseDate).toLocaleDateString()} + {(() => { + const d = new Date(charge.offenseDate); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString(); + })()}
Statute: {charge.statute} @@ -93,12 +125,23 @@ const SearchResult: React.FC = ({ searchResult: sr }) => { Fine: ${charge.fine.toFixed(2)}
)} + + {/* Per-charge filing agency: only shown when no top-level filing agency is present */} + {!summary.filingAgency && charge.filingAgency && ( +
+ Filing Agency: {charge.filingAgency} +
+ )}
{charge.dispositions && charge.dispositions.length > 0 && (
Disposition:{' '} {charge.dispositions[0].description} ( - {new Date(charge.dispositions[0].date).toLocaleDateString()}) + {(() => { + const d = new Date(charge.dispositions[0].date); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString(); + })()} + )
)} diff --git a/frontend/src/components/app/__tests__/SearchResult.test.tsx b/frontend/src/components/app/__tests__/SearchResult.test.tsx index 1ac0285..907ee93 100644 --- a/frontend/src/components/app/__tests__/SearchResult.test.tsx +++ b/frontend/src/components/app/__tests__/SearchResult.test.tsx @@ -50,8 +50,11 @@ const createTestCase = (override = {}): SearchResultType => ({ description: 'Guilty', }, ], + filingAgency: null, + filingAgencyAddress: [], }, ], + filingAgency: null, }, ...override, }); @@ -169,4 +172,126 @@ describe('SearchResult component', () => { const errorMessage = screen.getByText('Error: Failed to fetch case data'); expect(errorMessage).toHaveClass('text-sm', 'text-red-600'); }); + + it('displays arrest/citation date when present', () => { + const testCase = createTestCase({ + caseSummary: { + caseName: 'State vs. Doe', + court: 'Circuit Court', + arrestOrCitationDate: '2022-02-15T00:00:00Z', + arrestOrCitationType: 'Arrest', + charges: [], + }, + }); + + render(); + + // Label should be present and explicitly show 'Arrest Date' + expect(screen.getByText(/Arrest Date:/)).toBeInTheDocument(); + + // The displayed date should contain the year 2022 (locale independent check) + expect(screen.getByText(/2022/)).toBeInTheDocument(); + }); + + it('handles malformed charge dates gracefully', () => { + const testCase = createTestCase({ + caseSummary: { + caseName: 'State vs. Doe', + court: 'Circuit Court', + arrestOrCitationDate: 'not-a-date', + charges: [ + { + offenseDate: 'also-not-a-date', + filedDate: 'not-a-date', + description: 'Weird Charge', + statute: '000', + degree: { code: 'X', description: 'Unknown' }, + fine: 0, + dispositions: [{ date: 'bad-date', code: 'UNK', description: 'Disposition ' }], + }, + ], + }, + }); + + render(); + + // Should not render 'Invalid Date' anywhere + expect(screen.queryByText(/Invalid Date/)).not.toBeInTheDocument(); + + // Should still render the charge description + expect(screen.getByText('Weird Charge')).toBeInTheDocument(); + }); + + it('displays top-level filing agency when present', () => { + const testCase = createTestCase({ + caseSummary: { + caseName: 'State vs. Doe', + court: 'Circuit Court', + filingAgency: 'Metro PD', + charges: [ + { + offenseDate: '2022-01-01', + filedDate: '2022-01-02', + description: 'Theft', + statute: '123.456', + degree: { code: 'M', description: 'Misdemeanor' }, + fine: 0, + dispositions: [], + filingAgency: 'Metro PD', + }, + ], + }, + }); + + render(); + + // Top-level Filing Agency should be present + expect(screen.getByText(/Filing Agency:/)).toBeInTheDocument(); + expect(screen.getByText('Metro PD')).toBeInTheDocument(); + + // Per-charge filing agency should not be duplicated when top-level present + const chargeAgency = screen.queryAllByText(/Filing Agency:/).length; + expect(chargeAgency).toBe(1); // only the top-level label + }); + + it('displays per-charge filing agencies when they differ and no top-level is set', () => { + const testCase = createTestCase({ + caseSummary: { + caseName: 'State vs. Doe', + court: 'Circuit Court', + charges: [ + { + offenseDate: '2022-01-01', + filedDate: '2022-01-02', + description: 'Charge A', + statute: '111', + degree: { code: 'M', description: 'M' }, + fine: 0, + dispositions: [], + filingAgency: 'Dept A', + }, + { + offenseDate: '2022-02-01', + filedDate: '2022-02-02', + description: 'Charge B', + statute: '222', + degree: { code: 'M', description: 'M' }, + fine: 0, + dispositions: [], + filingAgency: 'Dept B', + }, + ], + }, + }); + + render(); + + // No single top-level filing agency — expect per-charge Filing Agency labels for each charge + const filingLabels = screen.queryAllByText(/Filing Agency:/); + expect(filingLabels.length).toBeGreaterThanOrEqual(2); + + // Both per-charge filing agencies should be present + expect(screen.getByText('Dept A')).toBeInTheDocument(); + expect(screen.getByText('Dept B')).toBeInTheDocument(); + }); }); diff --git a/serverless/lib/CaseProcessor.ts b/serverless/lib/CaseProcessor.ts index 93e54cb..c16c556 100644 --- a/serverless/lib/CaseProcessor.ts +++ b/serverless/lib/CaseProcessor.ts @@ -502,8 +502,32 @@ const caseEndpoints: Record = { financialSummary: { path: "Service/FinancialSummary('{caseId}')", }, + caseEvents: { + path: "Service/CaseEvents('{caseId}')?top=200", + }, }; +function parseMMddyyyyToDate(dateStr: string): Date | null { + if (!dateStr || typeof dateStr !== 'string') { + return null; + } + + const parts = dateStr.split('/'); + if (parts.length !== 3) { + return null; + } + + const month = parseInt(parts[0], 10); + const day = parseInt(parts[1], 10); + const year = parseInt(parts[2], 10); + + if (Number.isNaN(month) || Number.isNaN(day) || Number.isNaN(year)) { + return null; + } + + return new Date(Date.UTC(year, month - 1, day)); +} + async function fetchCaseSummary(caseId: string): Promise { try { const portalCaseUrl = process.env.PORTAL_CASE_URL; @@ -578,8 +602,8 @@ async function fetchCaseSummary(caseId: string): Promise { // Wait for all promises to resolve const results = await Promise.all(endpointPromises); - // Check if any endpoint failed - const requiredFailure = results.find(result => !result.success); + // Treat caseEvents as optional; if any other endpoint failed, consider it a required failure + const requiredFailure = results.find(result => !result.success && result.key !== 'caseEvents'); if (requiredFailure) { console.error(`Required endpoint ${requiredFailure.key} failed: ${requiredFailure.error}`); @@ -615,6 +639,7 @@ function buildCaseSummary(rawData: Record): CaseSumma caseName: rawData['summary']['CaseSummaryHeader']['Style'] || '', court: rawData['summary']['CaseSummaryHeader']['Heading'] || '', charges: [], + filingAgency: null, }; const chargeMap = new Map(); @@ -639,8 +664,21 @@ function buildCaseSummary(rawData: Record): CaseSumma }, fine: typeof chargeOffense['FineAmount'] === 'number' ? chargeOffense['FineAmount'] : 0, dispositions: [], + filingAgency: null, + filingAgencyAddress: [], }; + const filingAgencyRaw = chargeData['FilingAgencyDescription']; + if (filingAgencyRaw) { + charge.filingAgency = String(filingAgencyRaw).trim(); + } + + // Extract filing agency address if present. It will be an array of strings. + const filingAgencyAddressRaw = chargeData['FilingAgencyAddress']; + if (filingAgencyAddressRaw) { + charge.filingAgencyAddress.push(...(filingAgencyAddressRaw as any)); + } + // Add to charges array caseSummary.charges.push(charge); @@ -650,11 +688,27 @@ function buildCaseSummary(rawData: Record): CaseSumma } }); + // After processing charges, derive top-level filing agency if appropriate + try { + const definedAgencies = caseSummary.charges.map(ch => ch.filingAgency).filter((a): a is string => a !== null && a.length > 0); + + const uniqueAgencies = Array.from(new Set(definedAgencies)); + + // If there's at least one defined agency, and all defined agencies are identical, + // set it on the case summary. Charges that lack an agency (null) are ignored for this decision. + if (uniqueAgencies.length === 1 && uniqueAgencies[0]) { + caseSummary.filingAgency = uniqueAgencies[0]; + console.log(`🔔 Set Filing Agency to ${caseSummary.filingAgency}`); + } + } catch (faErr) { + console.error('Error computing top-level filing agency:', faErr); + } + // Process dispositions and link them to charges - const events = rawData['dispositionEvents']['Events'] || []; - console.log(`📋 Found ${events.length} disposition events`); + const dispositionEvents = rawData['dispositionEvents']['Events'] || []; + console.log(`📋 Found ${dispositionEvents.length} disposition events`); - events + dispositionEvents .filter( // eslint-disable-next-line @typescript-eslint/no-explicit-any (eventData: any) => eventData && eventData['Type'] === 'CriminalDispositionEvent' @@ -721,6 +775,60 @@ function buildCaseSummary(rawData: Record): CaseSumma }); }); + // Process case-level events to determine arrest or citation date (LPSD -> Arrest, CIT -> Citation) + try { + const caseEvents = rawData['caseEvents']?.['Events'] || []; + console.log(`📋 Found ${caseEvents.length} case events`); + + // Filter only events that have the LPSD (arrest) or CIT (citation) TypeId and a valid EventDate + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const candidateEvents = caseEvents.filter( + (ev: any) => ev && ev['Event'] && ev['Event']['TypeId'] && ev['Event']['TypeId']['Word'] && ev['Event']['EventDate'] + ); + + console.log(`🔎 Found ${candidateEvents.length} candidate events for arrest/citation`); + + if (candidateEvents.length > 0) { + const parsedCandidates: { date: Date; type: 'Arrest' | 'Citation'; raw: string }[] = []; + + candidateEvents.forEach((ev: any, idx: number) => { + const typeWord = ev['Event']['TypeId']['Word']; + const eventDateStr = ev['Event']['EventDate']; + + if (typeWord !== 'LPSD' && typeWord !== 'CIT') { + return; + } + + const parsed = parseMMddyyyyToDate(eventDateStr); + if (parsed) { + parsedCandidates.push({ + date: parsed, + type: typeWord === 'LPSD' ? 'Arrest' : 'Citation', + raw: eventDateStr, + }); + console.log(` ✔ Candidate #${idx}: Type=${typeWord}, Parsed=${parsed.toISOString()}`); + } else { + console.warn(` ✖ Candidate #${idx} has unparseable date: ${eventDateStr}`); + } + }); + + if (parsedCandidates.length > 0) { + // Choose the earliest date among all matching candidates + const earliest = parsedCandidates.reduce( + (min, c) => (c.date.getTime() < min.date.getTime() ? c : min), + parsedCandidates[0] + ); + caseSummary.arrestOrCitationDate = earliest.date.toISOString(); + caseSummary.arrestOrCitationType = earliest.type; + console.log(`🔔 Set ${earliest.type} date to ${caseSummary.arrestOrCitationDate}`); + } else { + console.log('No parsable arrest/citation dates found among candidates'); + } + } + } catch (evtErr) { + console.error('Error processing caseEvents for arrest/citation date:', evtErr); + } + return caseSummary; } catch (error) { AlertService.logError(Severity.ERROR, AlertCategory.SYSTEM, 'Error building case summary from raw data', error as Error, { @@ -736,6 +844,7 @@ const CaseProcessor = { processCaseData, queueCasesForSearch, fetchCaseIdFromPortal, + buildCaseSummary, }; export default CaseProcessor; diff --git a/serverless/lib/__tests__/CaseSearchProcessor.test.ts b/serverless/lib/__tests__/CaseSearchProcessor.test.ts index cde27db..9e5b3bd 100644 --- a/serverless/lib/__tests__/CaseSearchProcessor.test.ts +++ b/serverless/lib/__tests__/CaseSearchProcessor.test.ts @@ -40,6 +40,7 @@ describe('CaseSearchProcessor', () => { caseName: 'Test vs State', court: 'Test Court', charges: [], + filingAgency: null, }; const completeCase: SearchResult = { @@ -284,6 +285,7 @@ describe('CaseSearchProcessor', () => { caseName: 'Test vs State', court: 'Test Court', charges: [], + filingAgency: null, }; mockStorageClient.getSearchResults.mockResolvedValue({ diff --git a/serverless/lib/__tests__/caseProcessor.test.ts b/serverless/lib/__tests__/caseProcessor.test.ts index 760b67f..a0b07d7 100644 --- a/serverless/lib/__tests__/caseProcessor.test.ts +++ b/serverless/lib/__tests__/caseProcessor.test.ts @@ -140,4 +140,361 @@ describe('CaseProcessor', () => { expect(QueueClient.queueCasesForSearch).toHaveBeenCalledWith(cases, userId); }); }); + + // Tests for buildCaseSummary (moved from separate test file) + describe('buildCaseSummary', () => { + const { buildCaseSummary } = CaseProcessor as any; + + it('extracts the earliest LPSD Event.EventDate and sets arrestOrCitationDate and type as Arrest', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. Someone', + Heading: 'Circuit Court', + CaseId: 'case-123', + }, + }, + charges: { + Charges: [ + { + ChargeId: 1, + OffenseDate: '2020-01-01', + FiledDate: '2020-01-02', + ChargeOffense: { + ChargeOffenseDescription: 'Theft', + Statute: '123', + Degree: 'M', + DegreeDescription: 'Misdemeanor', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { + Events: [], + }, + caseEvents: { + Events: [ + { Event: { TypeId: { Word: 'LPSD' }, EventDate: '03/15/2021' } }, + { Event: { TypeId: { Word: 'LPSD' }, EventDate: '02/10/2021' } }, + { Event: { TypeId: { Word: 'OTHER' }, EventDate: '01/01/2020' } }, + ], + }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.arrestOrCitationDate).toBeDefined(); + expect(summary?.arrestOrCitationType).toBe('Arrest'); + + // Expected earliest LPSD date is 02/10/2021 -> construct UTC Date and compare ISO + const expectedIso = new Date(Date.UTC(2021, 1, 10)).toISOString(); + expect(summary?.arrestOrCitationDate).toBe(expectedIso); + }); + + it('selects CIT over LPSD if earlier (sets type Citation)', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. Someone', + Heading: 'Circuit Court', + CaseId: 'case-234', + }, + }, + charges: { Charges: [] }, + dispositionEvents: { Events: [] }, + caseEvents: { + Events: [ + { Event: { TypeId: { Word: 'LPSD' }, EventDate: '03/15/2021' } }, + { Event: { TypeId: { Word: 'CIT' }, EventDate: '02/09/2021' } }, + ], + }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.arrestOrCitationDate).toBeDefined(); + expect(summary?.arrestOrCitationType).toBe('Citation'); + + const expectedIso = new Date(Date.UTC(2021, 1, 9)).toISOString(); + expect(summary?.arrestOrCitationDate).toBe(expectedIso); + }); + + it('does not set arrestOrCitationDate when no LPSD/CIT events present', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. Someone', + Heading: 'Circuit Court', + CaseId: 'case-456', + }, + }, + charges: { + Charges: [], + }, + dispositionEvents: { + Events: [], + }, + caseEvents: { + Events: [{ Event: { TypeId: { Word: 'OTHER' }, EventDate: '03/15/2021' } }], + }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.arrestOrCitationDate).toBeUndefined(); + expect(summary?.arrestOrCitationType).toBeUndefined(); + }); + + it('ignores malformed LPSD Event.EventDate values', () => { + const { buildCaseSummary } = CaseProcessor as any; + + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. Someone', + Heading: 'Circuit Court', + CaseId: 'case-789', + }, + }, + charges: { Charges: [] }, + dispositionEvents: { Events: [] }, + caseEvents: { + Events: [ + { Event: { TypeId: { Word: 'LPSD' }, EventDate: 'not-a-date' } }, + { Event: { TypeId: { Word: 'LPSD' }, EventDate: '' } }, + { Event: { TypeId: { Word: 'LPSD' }, EventDate: null } }, + ], + }, + }; + + const summary = buildCaseSummary(rawData); + expect(summary).not.toBeNull(); + expect(summary?.arrestOrCitationDate).toBeUndefined(); + expect(summary?.arrestOrCitationType).toBeUndefined(); + }); + + it('sets top-level filing agency when single charge has filing agency', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. SingleCharge', + Heading: 'Circuit Court', + CaseId: 'case-f1', + }, + }, + charges: { + Charges: [ + { + ChargeId: 10, + OffenseDate: '2020-01-01', + FiledDate: '2020-01-02', + FilingAgencyDescription: 'Metro PD', + ChargeOffense: { + ChargeOffenseDescription: 'Assault', + Statute: '456', + Degree: 'F', + DegreeDescription: 'Felony', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { Events: [] }, + caseEvents: { Events: [] }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.filingAgency).toBe('Metro PD'); + expect(summary?.charges[0].filingAgency).toBe('Metro PD'); + }); + + it('sets top-level filing agency when multiple charges share same agency and some charges lack agency', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. MultiCharge', + Heading: 'Circuit Court', + CaseId: 'case-f2', + }, + }, + charges: { + Charges: [ + { + ChargeId: 11, + OffenseDate: '2020-01-01', + FiledDate: '2020-01-02', + FilingAgencyDescription: 'County Sheriff', + ChargeOffense: { + ChargeOffenseDescription: 'Burglary', + Statute: '789', + Degree: 'F', + DegreeDescription: 'Felony', + FineAmount: 0, + }, + }, + { + ChargeId: 12, + OffenseDate: '2020-02-01', + FiledDate: '2020-02-02', + // No FilingAgencyDescription on this charge + ChargeOffense: { + ChargeOffenseDescription: 'Robbery', + Statute: '321', + Degree: 'F', + DegreeDescription: 'Felony', + FineAmount: 0, + }, + }, + { + ChargeId: 13, + OffenseDate: '2020-03-01', + FiledDate: '2020-03-02', + FilingAgencyDescription: 'County Sheriff', + ChargeOffense: { + ChargeOffenseDescription: 'Theft', + Statute: '123', + Degree: 'M', + DegreeDescription: 'Misdemeanor', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { Events: [] }, + caseEvents: { Events: [] }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.filingAgency).toBe('County Sheriff'); + // Charges should retain any per-charge filingAgency where present + expect(summary?.charges.find((ch: any) => ch.offenseDate === '2020-01-01')?.filingAgency).toBe('County Sheriff'); + expect(summary?.charges.find((ch: any) => ch.offenseDate === '2020-02-01')?.filingAgency).toBeNull(); + }); + + it('does not set top-level filing agency when charges have differing agencies', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. DifferentAgencies', + Heading: 'Circuit Court', + CaseId: 'case-f3', + }, + }, + charges: { + Charges: [ + { + ChargeId: 21, + OffenseDate: '2020-04-01', + FiledDate: '2020-04-02', + FilingAgencyDescription: 'Dept A', + ChargeOffense: { + ChargeOffenseDescription: 'Charge A', + Statute: '111', + Degree: 'M', + DegreeDescription: 'M', + FineAmount: 0, + }, + }, + { + ChargeId: 22, + OffenseDate: '2020-05-01', + FiledDate: '2020-05-02', + FilingAgencyDescription: 'Dept B', + ChargeOffense: { + ChargeOffenseDescription: 'Charge B', + Statute: '222', + Degree: 'M', + DegreeDescription: 'M', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { Events: [] }, + caseEvents: { Events: [] }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.filingAgency).toBeNull(); + expect(summary?.charges[0].filingAgency).toBe('Dept A'); + expect(summary?.charges[1].filingAgency).toBe('Dept B'); + }); + + it('does not set filing agency when none present on charges', () => { + const rawData = { + summary: { CaseSummaryHeader: { Style: 'No Agency', Heading: 'Circuit Court', CaseId: 'case-f4' } }, + charges: { + Charges: [ + { + ChargeId: 31, + OffenseDate: '2020-06-01', + FiledDate: '2020-06-02', + ChargeOffense: { + ChargeOffenseDescription: 'NoAgency', + Statute: '000', + Degree: 'M', + DegreeDescription: 'M', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { Events: [] }, + caseEvents: { Events: [] }, + }; + + const summary = buildCaseSummary(rawData); + expect(summary).not.toBeNull(); + expect(summary?.filingAgency).toBeNull(); + expect(summary?.charges[0].filingAgency).toBeNull(); + expect(summary?.charges[0].filingAgencyAddress).toEqual([]); + }); + + it('sets filingAgencyAddress array on charge when provided as array', () => { + const rawData = { + summary: { + CaseSummaryHeader: { + Style: 'State vs. SingleCharge', + Heading: 'Circuit Court', + CaseId: 'case-fa1', + }, + }, + charges: { + Charges: [ + { + ChargeId: 50, + OffenseDate: '2020-07-01', + FiledDate: '2020-07-02', + FilingAgencyDescription: 'Metro PD', + FilingAgencyAddress: ['123 Main St', 'Suite 200'], + ChargeOffense: { + ChargeOffenseDescription: 'Assault', + Statute: '456', + Degree: 'F', + DegreeDescription: 'Felony', + FineAmount: 0, + }, + }, + ], + }, + dispositionEvents: { Events: [] }, + caseEvents: { Events: [] }, + }; + + const summary = buildCaseSummary(rawData); + + expect(summary).not.toBeNull(); + expect(summary?.charges[0].filingAgencyAddress).toEqual(['123 Main St', 'Suite 200']); + }); + }); }); diff --git a/serverless/lib/__tests__/storageClient.test.ts b/serverless/lib/__tests__/storageClient.test.ts index 1a9f265..aa71776 100644 --- a/serverless/lib/__tests__/storageClient.test.ts +++ b/serverless/lib/__tests__/storageClient.test.ts @@ -211,8 +211,8 @@ describe('StorageClient.getSearchResults resilience', () => { caseName: 'State vs Valid Defendant', court: 'Test Superior Court', charges: [ - { description: 'Charge 1', statute: 'ABC-123' }, - { description: 'Charge 2', statute: 'DEF-456' }, + { description: 'Charge 1', statute: 'ABC-123', filingAgency: null, filingAgencyAddress: [] }, + { description: 'Charge 2', statute: 'DEF-456', filingAgency: null, filingAgencyAddress: [] }, ], }; @@ -258,7 +258,7 @@ describe('StorageClient.getSearchResults resilience', () => { const summaryMissingName = { // caseName is missing court: 'Test Court', - charges: [{ description: 'Valid charge' }], + charges: [{ description: 'Valid charge', filingAgency: null, filingAgencyAddress: [] }], }; const result = await validateAndProcessCaseSummary(caseNumber, caseData, summaryMissingName); @@ -280,7 +280,7 @@ describe('StorageClient.getSearchResults resilience', () => { const summaryMissingCourt = { caseName: 'State vs Defendant', // court is missing - charges: [{ description: 'Valid charge' }], + charges: [{ description: 'Valid charge', filingAgency: null, filingAgencyAddress: [] }], }; const result = await validateAndProcessCaseSummary(caseNumber, caseData, summaryMissingCourt); @@ -360,8 +360,16 @@ describe('StorageClient.getSearchResults resilience', () => { { caseNumber: 'VALID001', caseData: { caseNumber: 'VALID001', caseId: 'id1', fetchStatus: { status: 'complete' } }, - summaryItem: { caseName: 'Valid Case 1', court: 'Court 1', charges: [{ description: 'Charge 1' }] }, - expectedResult: { caseName: 'Valid Case 1', court: 'Court 1', charges: [{ description: 'Charge 1' }] }, + summaryItem: { + caseName: 'Valid Case 1', + court: 'Court 1', + charges: [{ description: 'Charge 1', filingAgency: null, filingAgencyAddress: [] }], + }, + expectedResult: { + caseName: 'Valid Case 1', + court: 'Court 1', + charges: [{ description: 'Charge 1', filingAgency: null, filingAgencyAddress: [] }], + }, }, { caseNumber: 'CORRUPT002', @@ -372,8 +380,16 @@ describe('StorageClient.getSearchResults resilience', () => { { caseNumber: 'VALID003', caseData: { caseNumber: 'VALID003', caseId: 'id3', fetchStatus: { status: 'complete' } }, - summaryItem: { caseName: 'Valid Case 3', court: 'Court 3', charges: [{ description: 'Charge 3' }] }, - expectedResult: { caseName: 'Valid Case 3', court: 'Court 3', charges: [{ description: 'Charge 3' }] }, + summaryItem: { + caseName: 'Valid Case 3', + court: 'Court 3', + charges: [{ description: 'Charge 3', filingAgency: null, filingAgencyAddress: [] }], + }, + expectedResult: { + caseName: 'Valid Case 3', + court: 'Court 3', + charges: [{ description: 'Charge 3', filingAgency: null, filingAgencyAddress: [] }], + }, }, ]; @@ -421,5 +437,29 @@ describe('StorageClient.getSearchResults resilience', () => { // Verify cleanup was triggered for the corrupted case only expect(mockSetImmediate).toHaveBeenCalled(); }); + + it('should preserve arrestOrCitationDate and type when present in summary', async () => { + const { validateAndProcessCaseSummary } = require('../StorageClient'); + + const caseNumber = 'ARRESTDATE001'; + const caseData = { + caseNumber, + caseId: 'arrest-case-id', + fetchStatus: { status: 'complete' }, + lastUpdated: '2025-09-19T12:00:00Z', + }; + + const validSummaryItem = { + caseName: 'State vs Arrested', + court: 'Test Court', + charges: [], + arrestOrCitationDate: '2021-02-10T00:00:00.000Z', + arrestOrCitationType: 'Arrest', + }; + + const result = await validateAndProcessCaseSummary(caseNumber, caseData, validSummaryItem); + + expect(result).toEqual(validSummaryItem); + }); }); }); diff --git a/shared/types/ZipCase.ts b/shared/types/ZipCase.ts index 4b52fa9..368ffac 100644 --- a/shared/types/ZipCase.ts +++ b/shared/types/ZipCase.ts @@ -26,12 +26,19 @@ export interface Charge { degree: ChargeDegree; fine: number; dispositions: Disposition[]; + filingAgency: string | null; + filingAgencyAddress: string[]; } +export type ArrestOrCitationType = 'Arrest' | 'Citation'; + export interface CaseSummary { caseName: string; court: string; charges: Charge[]; + arrestOrCitationDate?: string; + arrestOrCitationType?: ArrestOrCitationType; + filingAgency: string | null; } export interface ZipCase {