From 4c5f2253019b15a4b69b1f661a78fec470f07e2f Mon Sep 17 00:00:00 2001 From: Jim Lerza Date: Fri, 10 Jan 2025 23:06:16 -0500 Subject: [PATCH] opex: first pass at gathering LEA statistics for the pro-se committee --- scripts/reports/lea-stats.ts | 191 +++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100755 scripts/reports/lea-stats.ts diff --git a/scripts/reports/lea-stats.ts b/scripts/reports/lea-stats.ts new file mode 100755 index 00000000000..b83e690ad6b --- /dev/null +++ b/scripts/reports/lea-stats.ts @@ -0,0 +1,191 @@ +#!/usr/bin/env -S npx ts-node --transpile-only + +import { + type ScriptConfig, + parseArgsAndEnvVars, +} from '../helpers/parseArgsAndEnvVars'; +import { + type ServerApplicationContext, + createApplicationContext, +} from '@web-api/applicationContext'; +import { generateCsv } from '../helpers/generate-csv'; +import { getCaseByDocketNumber } from '@web-api/persistence/dynamo/cases/getCaseByDocketNumber'; +import { searchAll } from '@web-api/persistence/elasticsearch/searchClient'; +import PQueue from 'p-queue'; + +const scriptConfig: ScriptConfig = { + description: + 'lea-stats - Generates statistics related to Limited Entry of Appearance documents.', + environment: { + env: 'ENV', + region: 'REGION', + }, + parameters: { + fiscal: { + short: 'f', + type: 'boolean', + }, + year: { + position: 0, + required: true, + transform: 'number', + type: 'string', + }, + }, + requireActiveAwsSession: true, +}; + +const OUTPUT_DIR = `${process.env.HOME}/Documents`; +const caseCache: { [k: string]: RawCase } = {}; +const concurrency = 50; + +const getLEAsFiledInYear = async ({ + applicationContext, + fiscal, + year, +}: { + applicationContext: ServerApplicationContext; + fiscal: boolean; + year: number; +}): Promise => { + const { results } = await searchAll({ + applicationContext, + searchParameters: { + body: { + query: { + bool: { + must: [ + { + term: { + 'entityName.S': 'DocketEntry', + }, + }, + { + term: { + 'eventCode.S': 'LEA', + }, + }, + { + range: { + 'receivedAt.S': { + gte: fiscal + ? `${year - 1}-10-01T05:00:00Z` + : `${year}-01-01T04:00:00Z`, + lt: fiscal + ? `${year}-10-01T05:00:00Z` + : `${year + 1}-01-01T04:00:00Z`, + }, + }, + }, + ], + }, + }, + sort: [{ 'receivedAt.S': 'asc' }], + }, + index: 'efcms-docket-entry', + }, + }); + return results; +}; + +const getCaseEntity = async ({ + applicationContext, + docketNumber, +}: { + applicationContext: ServerApplicationContext; + docketNumber: string; +}): Promise => { + if (!(docketNumber in caseCache)) { + caseCache[docketNumber] = await getCaseByDocketNumber({ + applicationContext, + docketNumber, + includeConsolidatedCases: false, + }); + } + return caseCache[docketNumber]; +}; + +const getNocFiledAfterLeaInCase = ({ + docketNumber, + leaReceivedAt, +}: { + docketNumber: string; + leaReceivedAt: string; +}): RawDocketEntry | undefined => { + if (!(docketNumber in caseCache)) { + return; + } + const subsequentNOCs = caseCache[docketNumber].docketEntries.filter(de => { + return de.eventCode === 'NOC' && de.receivedAt > leaReceivedAt; + }); + if (!subsequentNOCs) { + return; + } + return subsequentNOCs[0]; +}; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const { fiscal, year } = parseArgsAndEnvVars(scriptConfig) as { + fiscal: boolean; + year: number; + }; + const applicationContext = createApplicationContext({}); + + const leas = await getLEAsFiledInYear({ applicationContext, fiscal, year }); + console.log( + `Found ${leas.length} Limited Entry of Appearance documents filed in ` + + `in ${fiscal ? 'fiscal' : 'calendar'} year ${year}.`, + ); + const docketNumbers = [...new Set(leas.map(de => de.docketNumber))]; + const queue = new PQueue({ concurrency }); + const funcs = docketNumbers.map( + (docketNumber: string) => async () => + await getCaseEntity({ applicationContext, docketNumber }), + ); + await queue.addAll(funcs); + + const procedureTypeAggs: { [k: string]: number } = {}; + for (const caseEntity of Object.values(caseCache)) { + if (!(caseEntity.procedureType in procedureTypeAggs)) { + procedureTypeAggs[caseEntity.procedureType] = 0; + } + procedureTypeAggs[caseEntity.procedureType]++; + } + const stats = { + procedureTypeAggs, + totalLeas: leas.length, + totalSubsequentNocs: 0, + }; + const rows: {}[] = []; + for (const lea of leas) { + const subsequentNoc = getNocFiledAfterLeaInCase({ + docketNumber: lea.docketNumber, + leaReceivedAt: lea.receivedAt, + }); + if (subsequentNoc) { + stats.totalSubsequentNocs++; + } + rows.push({ + docketNumber: lea.docketNumber, + leaFiledDate: lea.receivedAt.split('T')[0], + leaIndex: lea.index, + nocFiledDate: subsequentNoc?.receivedAt.split('T')[0] ?? '', + nocIndex: subsequentNoc?.index ?? '', + procedureType: caseCache[lea.docketNumber].procedureType, + }); + } + + const columns = [ + { header: 'Docket Number', key: 'docketNumber' }, + { header: 'Procedure Type', key: 'procedureType' }, + { header: 'LEA Index', key: 'leaIndex' }, + { header: 'LEA Filed', key: 'leaFiledDate' }, + { header: 'Subsequent NOC Index', key: 'nocIndex' }, + { header: 'Subsequent NOC Filed', key: 'nocFiledDate' }, + ]; + const filename = `${OUTPUT_DIR}/leas-filed-in${fiscal ? '-fiscal-year' : ''}-${year}.csv`; + generateCsv({ columns, filename, rows }); + console.log(`Generated ${filename}`); + console.log('\nStatistics:', stats); +})();