diff --git a/cypress/cypress-integration/integration/private-practitioner-mobile.cy.ts b/cypress/cypress-integration/integration/private-practitioner-mobile.cy.ts new file mode 100644 index 00000000000..4e95536966b --- /dev/null +++ b/cypress/cypress-integration/integration/private-practitioner-mobile.cy.ts @@ -0,0 +1,26 @@ +import { navigateTo as navigateToDashboard } from '../support/pages/dashboard'; + +describe('Private practitioner mobile', () => { + beforeEach(() => { + cy.viewport('iphone-6'); + }); + + it('should have access to case, order, and opinion advanced searches (and NOT practitioner advanced search)', () => { + navigateToDashboard('privatePractitioner'); + + cy.get('[data-test="advanced-search-link"]').click(); + + cy.get('[data-test="advanced-search-type-mobile-selector"]') + .children() + .then(options => { + const actualAdvancedSearchOptions: string[] = [...options].map( + o => o.innerText, + ); + + const expectedAdvancedSearchOptions = ['Case', 'Order', 'Opinion']; + expect(actualAdvancedSearchOptions).to.deep.eq( + expectedAdvancedSearchOptions, + ); + }); + }); +}); diff --git a/cypress/cypress-integration/support/pages/document-qc.ts b/cypress/cypress-integration/support/pages/document-qc.ts index 2849391afac..85391fc6734 100644 --- a/cypress/cypress-integration/support/pages/document-qc.ts +++ b/cypress/cypress-integration/support/pages/document-qc.ts @@ -48,6 +48,12 @@ export const uploadCourtIssuedDocumentAndEditViaDocumentQC = attempt => { const freeText = `court document ${attempt}`; cy.get('#upload-description').clear().type(freeText); cy.get('input#primary-document-file').attachFile('../fixtures/w3-dummy.pdf'); + + // Fix flaky test + // https://github.com/flexion/ef-cms/issues/10144 + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(0); + cy.get('#save-uploaded-pdf-button').click(); cy.get('#add-court-issued-docket-entry-button').click(); cy.get('#document-type .select-react-element__input-container input') diff --git a/cypress/cypress-smoketests/integration/court-issued-documents.cy.ts b/cypress/cypress-smoketests/integration/court-issued-documents.cy.ts index 2dd39bf013b..7cb31ece292 100644 --- a/cypress/cypress-smoketests/integration/court-issued-documents.cy.ts +++ b/cypress/cypress-smoketests/integration/court-issued-documents.cy.ts @@ -155,6 +155,10 @@ describe('Docket Clerk', () => { // in its own step for retry purposes - sometimes the click fails it('should click the save uploaded PDF button', () => { + // Fix flaky test + // https://github.com/flexion/ef-cms/issues/10144 + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(0); clickSaveUploadedPdfButton(); }); diff --git a/package-lock.json b/package-lock.json index 7620496b49a..73a128b18a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,7 @@ "@types/aws-lambda": "^8.10.119", "@types/jest": "^29.5.4", "@types/lodash": "^4.14.197", + "@types/luxon": "^3.3.0", "@types/node": "^20.5.7", "@types/promise-retry": "^1.1.3", "@types/react": "^18.2.21", @@ -8011,6 +8012,12 @@ "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz", + "integrity": "sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", diff --git a/package.json b/package.json index 3000c33795c..b4bdf7750e2 100644 --- a/package.json +++ b/package.json @@ -257,6 +257,7 @@ "@types/aws-lambda": "^8.10.119", "@types/jest": "^29.5.4", "@types/lodash": "^4.14.197", + "@types/luxon": "^3.3.0", "@types/node": "^20.5.7", "@types/promise-retry": "^1.1.3", "@types/react": "^18.2.21", diff --git a/scripts/dynamo/setup-use-change-of-address-lambda-flag.sh b/scripts/dynamo/setup-use-change-of-address-lambda-flag.sh new file mode 100755 index 00000000000..19166f63441 --- /dev/null +++ b/scripts/dynamo/setup-use-change-of-address-lambda-flag.sh @@ -0,0 +1,13 @@ +#!/bin/bash -e + +# Sets the redaction acknowledgement enabled feature flag to "false" in the dynamo deploy table + +# Usage +# ENV=dev ./setup-redaction-acknowledgement-enabled-flag.sh + +./check-env-variables.sh \ + "ENV" \ + "AWS_SECRET_ACCESS_KEY" \ + "AWS_ACCESS_KEY_ID" + +aws dynamodb put-item --region us-east-1 --table-name "efcms-deploy-${ENV}" --item '{"pk":{"S":"use-change-of-address-lambda"},"sk":{"S":"use-change-of-address-lambda"},"current":{"BOOL":true}}' diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index 13322ade61a..3667fd8c04c 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -104,6 +104,11 @@ export const ALLOWLIST_FEATURE_FLAGS = { disabledMessage: 'Currently using legacy trial status types.', key: 'updated-trial-status-types', }, + USE_CHANGE_OF_ADDRESS_LAMBDA: { + disabledMessage: + 'A flag to know when to use the change of address lambda for processing.', + key: 'use-change-of-address-lambda', + }, USE_EXTERNAL_PDF_GENERATION: { disabledMessage: 'A flag to tell the code to directly generation pdfs or to do in an external lambda.', diff --git a/shared/src/business/entities/Practitioner.ts b/shared/src/business/entities/Practitioner.ts index 598bc767b89..50cd535f476 100644 --- a/shared/src/business/entities/Practitioner.ts +++ b/shared/src/business/entities/Practitioner.ts @@ -13,12 +13,12 @@ import { User } from './User'; import joi from 'joi'; export class Practitioner extends User { - public additionalPhone: string; + public additionalPhone?: string; public admissionsDate: string; public admissionsStatus: string; public barNumber: string; public birthYear: string; - public confirmEmail: string; + public confirmEmail?: string; public employer: string; public firmName: string; public firstName: string; diff --git a/shared/src/business/test/createTestApplicationContext.ts b/shared/src/business/test/createTestApplicationContext.ts index 24c0fc2cb96..8083e173b21 100644 --- a/shared/src/business/test/createTestApplicationContext.ts +++ b/shared/src/business/test/createTestApplicationContext.ts @@ -65,6 +65,7 @@ import { } from '../../../src/business/utilities/getFormattedJudgeName'; import { formatPhoneNumber } from '../../../src/business/utilities/formatPhoneNumber'; import { generateAndServeDocketEntry } from '../useCaseHelper/service/createChangeItems'; +import { generateChangeOfAddressHelper } from '@shared/business/useCaseHelper/generateChangeOfAddressHelper'; import { generateNoticesForCaseTrialSessionCalendarInteractor } from '../useCases/trialSessions/generateNoticesForCaseTrialSessionCalendarInteractor'; import { getAddressPhoneDiff, @@ -378,6 +379,9 @@ export const createTestApplicationContext = ({ user } = {}) => { generateAndServeDocketEntry: jest .fn() .mockImplementation(generateAndServeDocketEntry), + generateChangeOfAddressHelper: jest + .fn() + .mockImplementation(generateChangeOfAddressHelper), getJudgeInSectionHelper: jest.fn(), getUserIdForNote: jest.fn().mockImplementation(getUserIdForNote), removeCounselFromRemovedPetitioner: jest diff --git a/shared/src/business/useCaseHelper/coverSheets/removeCoversheet.ts b/shared/src/business/useCaseHelper/coverSheets/removeCoversheet.ts index 27353bb1902..2c5851329ff 100644 --- a/shared/src/business/useCaseHelper/coverSheets/removeCoversheet.ts +++ b/shared/src/business/useCaseHelper/coverSheets/removeCoversheet.ts @@ -20,8 +20,9 @@ export const removeCoversheet = async ( }) .promise()); } catch (err) { - err.message = `${err.message} docket entry id is ${docketEntryId}`; - throw err; + const error = err as Error; + error.message = `${error.message} docket entry id is ${docketEntryId}`; + throw error; } const { PDFDocument } = await applicationContext.getPdfLib(); diff --git a/shared/src/business/useCaseHelper/generateChangeOfAddressHelper.ts b/shared/src/business/useCaseHelper/generateChangeOfAddressHelper.ts new file mode 100644 index 00000000000..00b60dbad85 --- /dev/null +++ b/shared/src/business/useCaseHelper/generateChangeOfAddressHelper.ts @@ -0,0 +1,196 @@ +import { Case } from '@shared/business/entities/cases/Case'; +import { Practitioner } from '../entities/Practitioner'; +import { + ROLES, + SERVICE_INDICATOR_TYPES, +} from '@shared/business/entities/EntityConstants'; +import { TUserContact } from '@shared/business/useCases/users/generateChangeOfAddress'; +import { aggregatePartiesForService } from '@shared/business/utilities/aggregatePartiesForService'; +import { clone } from 'lodash'; +import { generateAndServeDocketEntry } from '@shared/business/useCaseHelper/service/createChangeItems'; + +/** + * generateChangeOfAddressHelper + * + * @param {object} applicationContext the application context + * @param {string} providers.deadlineDate the date of the deadline to generated + * @param {string} providers.description the description of the deadline + * @param {Case} providers.subjectCaseEntity the subjectCaseEntity + */ +export const generateChangeOfAddressHelper = async ({ + applicationContext, + bypassDocketEntry, + contactInfo, + docketNumber, + firmName, + jobId, + requestUserId, + updatedEmail, + updatedName, + user, + websocketMessagePrefix, +}: { + applicationContext: IApplicationContext; + docketNumber: string; + bypassDocketEntry: boolean; + contactInfo: TUserContact; + firmName: string; + updatedEmail?: string; + updatedName?: string; + jobId: string; + user: RawPractitioner; + requestUserId?: string; + websocketMessagePrefix: string; +}) => { + try { + const newData = contactInfo; + + const userCase = await applicationContext + .getPersistenceGateway() + .getCaseByDocketNumber({ + applicationContext, + docketNumber, + }); + let caseEntity = new Case(userCase, { applicationContext }); + + const practitionerName = updatedName || user.name; + const practitionerObject = caseEntity.privatePractitioners + .concat(caseEntity.irsPractitioners) + .find(practitioner => practitioner.userId === user.userId); + + if (!practitionerObject) { + throw new Error( + `Could not find user|${user.userId} barNumber: ${user.barNumber} on ${docketNumber}`, + ); + } + + const oldData = clone(practitionerObject.contact); + + // This updates the case by reference! + practitionerObject.contact = contactInfo; + practitionerObject.firmName = firmName; + practitionerObject.name = practitionerName; + + if (!oldData.email && updatedEmail) { + practitionerObject.serviceIndicator = + SERVICE_INDICATOR_TYPES.SI_ELECTRONIC; + practitionerObject.email = updatedEmail; + } + + if (!bypassDocketEntry && caseEntity.shouldGenerateNoticesForCase()) { + await prepareToGenerateAndServeDocketEntry({ + applicationContext, + caseEntity, + newData, + oldData, + practitionerName, + user, + }); + } + + await applicationContext.getUseCaseHelpers().updateCaseAndAssociations({ + applicationContext, + caseToUpdate: caseEntity, + }); + } catch (error) { + applicationContext.logger.error(error); + } + + await applicationContext.getNotificationGateway().sendNotificationToUser({ + applicationContext, + message: { + action: `${websocketMessagePrefix}_contact_update_progress`, + }, + userId: requestUserId || user.userId, + }); + + const updatedJob = await applicationContext + .getPersistenceGateway() + .setChangeOfAddressCaseAsDone({ applicationContext, docketNumber, jobId }); + + const isDoneProcessing = updatedJob.remaining === 0; + + if (isDoneProcessing) { + applicationContext.logger.info( + `"change-of-address-job|${jobId}" job finished`, + ); + + if (websocketMessagePrefix === 'user') { + const userEntity = new Practitioner({ + ...user, + isUpdatingInformation: false, + }); + + await applicationContext.getPersistenceGateway().updateUser({ + applicationContext, + user: userEntity.validate().toRawObject(), + }); + } + + await applicationContext.getNotificationGateway().sendNotificationToUser({ + applicationContext, + message: { + action: `${websocketMessagePrefix}_contact_full_update_complete`, + }, + userId: requestUserId || user.userId, + }); + } +}; + +/** + * This function prepares data to be passed to generateAndServeDocketEntry + * + * @param {object} providers the providers object + * @param {object} providers.applicationContext the application context + * @param {object} providers.caseEntity the instantiated Case class + * @param {object} providers.newData the new practitioner contact information + * @param {object} providers.oldData the old practitioner contact information (for comparison) + * @param {object} providers.practitionerName the name of the practitioner + * @param {object} providers.user the user object that includes userId, barNumber etc. + * @returns {Promise<*>} resolves upon completion of docket entry service + */ +const prepareToGenerateAndServeDocketEntry = async ({ + applicationContext, + caseEntity, + newData, + oldData, + practitionerName, + user, +}) => { + const documentType = applicationContext + .getUtilities() + .getDocumentTypeForAddressChange({ + newData, + oldData, + }); + + if (!documentType) return; + + const servedParties = aggregatePartiesForService(caseEntity); + + const docketMeta = {} as any; + if (user.role === ROLES.privatePractitioner) { + docketMeta.privatePractitioners = [ + { + name: practitionerName, + }, + ]; + } else if (user.role === ROLES.irsPractitioner) { + docketMeta.partyIrsPractitioner = true; + } + + newData.name = practitionerName; + const { changeOfAddressDocketEntry } = await generateAndServeDocketEntry({ + applicationContext, + barNumber: user.barNumber, + caseEntity, + docketMeta, + documentType, + newData, + oldData, + servedParties, + user, + }); + + caseEntity.updateDocketEntry(changeOfAddressDocketEntry); +}; diff --git a/shared/src/business/useCaseHelper/generatePdfFromHtmlHelper.ts b/shared/src/business/useCaseHelper/generatePdfFromHtmlHelper.ts index 5bdce2f2c78..00b0ccfacc5 100644 --- a/shared/src/business/useCaseHelper/generatePdfFromHtmlHelper.ts +++ b/shared/src/business/useCaseHelper/generatePdfFromHtmlHelper.ts @@ -23,10 +23,10 @@ export const generatePdfFromHtmlHelper = async ( }: { contentHtml: string; displayHeaderFooter: boolean; - docketNumber: string; - footerHtml: string; - headerHtml: string; - overwriteFooter: string; + docketNumber?: string; + footerHtml?: string; + headerHtml?: string; + overwriteFooter?: string; }, ) => { let browser: Browser | undefined; diff --git a/shared/src/business/useCases/addCoverToPdf.ts b/shared/src/business/useCases/addCoverToPdf.ts index 4d553af5858..dcd7716f35a 100644 --- a/shared/src/business/useCases/addCoverToPdf.ts +++ b/shared/src/business/useCases/addCoverToPdf.ts @@ -1,3 +1,4 @@ +import { Case } from '../entities/cases/Case'; import { DocketEntry } from '../entities/DocketEntry'; import { generateCoverSheetData } from './generateCoverSheetData'; diff --git a/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.test.ts b/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.test.ts index 01d5bc85a32..094f8b860a2 100644 --- a/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.test.ts +++ b/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.test.ts @@ -100,7 +100,7 @@ describe('checkForReadyForTrialCasesInteractor', () => { ).not.toHaveBeenCalled(); }); - it("should update cases to 'ready for trial' that meet requirements", async () => { + it("should update cases to 'ready for trial' that meet requirements, removing duplicate cases before updating", async () => { /** * Requirements: * 1. Case has status 'General Docket - Not at Issue' @@ -123,6 +123,8 @@ describe('checkForReadyForTrialCasesInteractor', () => { applicationContext .getPersistenceGateway() .getReadyForTrialCases.mockReturnValue([ + { docketNumber: '101-20' }, + { docketNumber: '101-20' }, { docketNumber: '101-20' }, { docketNumber: '320-21' }, ]); diff --git a/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.ts b/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.ts index 6a6b6c51188..9cb19bdbb02 100644 --- a/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.ts +++ b/shared/src/business/useCases/checkForReadyForTrialCasesInteractor.ts @@ -1,6 +1,7 @@ import { CASE_STATUS_TYPES } from '../entities/EntityConstants'; import { Case } from '../entities/cases/Case'; import { createISODateString } from '../utilities/DateHandler'; +import { uniqBy } from 'lodash'; /** * @param {object} applicationContext the application context @@ -10,10 +11,12 @@ export const checkForReadyForTrialCasesInteractor = async ( ) => { applicationContext.logger.debug('Time', createISODateString()); - const caseCatalog = await applicationContext + const docketNumbers: { docketNumber: string }[] = await applicationContext .getPersistenceGateway() .getReadyForTrialCases({ applicationContext }); + const caseCatalog = uniqBy(docketNumbers, 'docketNumber'); + const updateForTrial = async entity => { // assuming we want these done serially; if first fails, promise is rejected and error thrown const caseEntity = entity.validate(); @@ -33,7 +36,7 @@ export const checkForReadyForTrialCasesInteractor = async ( } }; - const updatedCases = []; + const caseUpdatePromises: Promise[] = []; for (let caseRecord of caseCatalog) { const { docketNumber } = caseRecord; @@ -52,13 +55,13 @@ export const checkForReadyForTrialCasesInteractor = async ( if ( caseEntity.status === CASE_STATUS_TYPES.generalDocketReadyForTrial ) { - updatedCases.push(updateForTrial(caseEntity)); + caseUpdatePromises.push(updateForTrial(caseEntity)); } } } } - await Promise.all(updatedCases); + await Promise.all(caseUpdatePromises); applicationContext.logger.debug('Time', createISODateString()); }; diff --git a/shared/src/business/useCases/generateCoverSheetData.ts b/shared/src/business/useCases/generateCoverSheetData.ts index af40b824c85..ff9567d4083 100644 --- a/shared/src/business/useCases/generateCoverSheetData.ts +++ b/shared/src/business/useCases/generateCoverSheetData.ts @@ -18,6 +18,10 @@ export const formatCaseTitle = ({ applicationContext, caseEntity, useInitialData, +}: { + applicationContext: IApplicationContext; + caseEntity: Case; + useInitialData?: boolean; }) => { const caseCaption = useInitialData ? caseEntity.initialCaption @@ -54,7 +58,7 @@ export const generateCoverSheetData = async ({ docketEntryEntity: DocketEntry; filingDateUpdated: boolean; stampData?: any; - useInitialData: boolean; + useInitialData?: boolean; }) => { const dateServedFormatted = docketEntryEntity.servedAt ? formatDateString(docketEntryEntity.servedAt, FORMATS.MMDDYY) diff --git a/shared/src/business/useCases/generatePractitionerCaseListPdfInteractor.ts b/shared/src/business/useCases/generatePractitionerCaseListPdfInteractor.ts index 50c21792c51..3a8599352df 100644 --- a/shared/src/business/useCases/generatePractitionerCaseListPdfInteractor.ts +++ b/shared/src/business/useCases/generatePractitionerCaseListPdfInteractor.ts @@ -17,7 +17,7 @@ import { partition } from 'lodash'; export const generatePractitionerCaseListPdfInteractor = async ( applicationContext: IApplicationContext, { userId }: { userId: string }, -): Promise => { +) => { const user = applicationContext.getCurrentUser(); if (!isAuthorized(user, ROLE_PERMISSIONS.VIEW_PRACTITIONER_CASE_LIST)) { diff --git a/shared/src/business/useCases/judgeActivityReport/getCasesByStatusAndByJudgeInteractor.test.ts b/shared/src/business/useCases/judgeActivityReport/getCasesByStatusAndByJudgeInteractor.test.ts index 1eb78f1d2ab..aaecc94729f 100644 --- a/shared/src/business/useCases/judgeActivityReport/getCasesByStatusAndByJudgeInteractor.test.ts +++ b/shared/src/business/useCases/judgeActivityReport/getCasesByStatusAndByJudgeInteractor.test.ts @@ -1,6 +1,7 @@ import { CASE_STATUS_TYPES, CAV_AND_SUBMITTED_CASES_PAGE_SIZE, + STATUS_OF_MATTER_OPTIONS, } from '@shared/business/entities/EntityConstants'; import { FORMATS } from '@shared/business/utilities/DateHandler'; import { @@ -11,6 +12,7 @@ import { MOCK_SUBMITTED_CASE_WITH_ODD_ON_DOCKET_RECORD, MOCK_SUBMITTED_CASE_WITH_SDEC_ON_DOCKET_RECORD, } from '@shared/test/mockCase'; +import { RawCaseWorksheet } from '@shared/business/entities/caseWorksheet/CaseWorksheet'; import { applicationContext } from '../../test/createTestApplicationContext'; import { getCasesByStatusAndByJudgeInteractor } from './getCasesByStatusAndByJudgeInteractor'; import { judgeUser, petitionsClerkUser } from '@shared/test/mockUsers'; @@ -57,6 +59,12 @@ describe('getCasesByStatusAndByJudgeInteractor', () => { petitioners: [], status: CASE_STATUS_TYPES.cav, }; + const mockCaseWorksheet = { + docketNumber: '101-20', + finalBriefDueDate: '01-01-2022', + primaryIssue: 'nothing', + statusOfMatter: STATUS_OF_MATTER_OPTIONS[1], + } as RawCaseWorksheet; beforeAll(() => { applicationContext.getSearchClient().count = jest.fn(); @@ -65,6 +73,9 @@ describe('getCasesByStatusAndByJudgeInteractor', () => { .getDocketNumbersByStatusAndByJudge.mockImplementation( () => mockReturnedDocketNumbers, ); + applicationContext + .getPersistenceGateway() + .getCaseWorksheet.mockImplementation(() => mockCaseWorksheet); }); beforeEach(() => { @@ -110,7 +121,7 @@ describe('getCasesByStatusAndByJudgeInteractor', () => { }); }); - it(`should return an array of 1 case (stripping out the cases with served ${prohibitedDocketEntries} docket entries and no consolidated cases)`, async () => { + it(`should return an array of 1 case (stripping out the cases with served ${prohibitedDocketEntries} docket entries, no consolidated cases, or no caseStatusHistory)`, async () => { mockReturnedDocketNumbers = [ { ...mockCaseInfo, docketNumber: MOCK_SUBMITTED_CASE.docketNumber }, { @@ -196,4 +207,48 @@ describe('getCasesByStatusAndByJudgeInteractor', () => { expect(result.totalCount).toEqual(1); }); + + it('should add a caseWorksheet field to cases returned', async () => { + mockReturnedDocketNumbers = [ + { ...mockCaseInfo, docketNumber: '101-23' }, + { + ...mockCaseInfo, + docketNumber: '102-23', + }, + ]; + + mockReturnedDocketNumbersToFilterOut = []; + + applicationContext + .getPersistenceGateway() + .getDocketNumbersByStatusAndByJudge.mockReturnValue( + mockReturnedDocketNumbers, + ); + + applicationContext + .getPersistenceGateway() + .getDocketNumbersWithServedEventCodes.mockReturnValue( + mockReturnedDocketNumbersToFilterOut, + ); + + const result = await getCasesByStatusAndByJudgeInteractor( + applicationContext, + mockValidRequest, + ); + + expect(result.cases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + caseWorksheet: mockCaseWorksheet, + docketNumber: '101-23', + }), + expect.objectContaining({ + caseWorksheet: mockCaseWorksheet, + docketNumber: '102-23', + }), + ]), + ); + + expect(result.totalCount).toEqual(2); + }); }); diff --git a/shared/src/business/useCases/practitioners/updatePractitionerUserInteractor.ts b/shared/src/business/useCases/practitioners/updatePractitionerUserInteractor.ts index ddf1bc71127..9b4cba4525c 100644 --- a/shared/src/business/useCases/practitioners/updatePractitionerUserInteractor.ts +++ b/shared/src/business/useCases/practitioners/updatePractitionerUserInteractor.ts @@ -174,13 +174,13 @@ export const updatePractitionerUserInteractor = async ( user: oldUser, websocketMessagePrefix: 'admin', }); + } else { + await applicationContext.getNotificationGateway().sendNotificationToUser({ + applicationContext, + message: { + action: 'admin_contact_full_update_complete', + }, + userId: requestUser.userId, + }); } - - await applicationContext.getNotificationGateway().sendNotificationToUser({ - applicationContext, - message: { - action: 'admin_contact_full_update_complete', - }, - userId: requestUser.userId, - }); }; diff --git a/shared/src/business/useCases/users/generateChangeOfAddress.ts b/shared/src/business/useCases/users/generateChangeOfAddress.ts index 8886a0022f4..b5895ffdaa3 100644 --- a/shared/src/business/useCases/users/generateChangeOfAddress.ts +++ b/shared/src/business/useCases/users/generateChangeOfAddress.ts @@ -1,10 +1,6 @@ -import { Case } from '../../entities/cases/Case'; -import { ROLES, SERVICE_INDICATOR_TYPES } from '../../entities/EntityConstants'; -import { aggregatePartiesForService } from '../../utilities/aggregatePartiesForService'; -import { clone } from 'lodash'; -import { generateAndServeDocketEntry } from '../../useCaseHelper/service/createChangeItems'; +import { ALLOWLIST_FEATURE_FLAGS } from '../../entities/EntityConstants'; -type TUserContact = { +export type TUserContact = { address1: string; address2: string; address3: string; @@ -52,14 +48,14 @@ const generateChangeOfAddressForPractitioner = async ({ user: any; websocketMessagePrefix?: string; }) => { - const docketNumbers = await applicationContext + const associatedUserCases = await applicationContext .getPersistenceGateway() - .getCasesByUserId({ + .getCasesForUser({ applicationContext, userId: user.userId, }); - if (docketNumbers.length === 0) { + if (associatedUserCases.length === 0) { return []; } @@ -69,147 +65,77 @@ const generateChangeOfAddressForPractitioner = async ({ message: { action: `${websocketMessagePrefix}_contact_update_progress`, completedCases, - totalCases: docketNumbers.length, + totalCases: associatedUserCases.length, }, userId: requestUserId || user.userId, }); - const updatedCases = []; + const featureFlags = await applicationContext + .getUseCases() + .getAllFeatureFlagsInteractor(applicationContext); - for (let caseInfo of docketNumbers) { - try { - const { docketNumber } = caseInfo; - const newData = contactInfo; + const isChangeOfAddressLambdaEnabled = + featureFlags[ALLOWLIST_FEATURE_FLAGS.USE_CHANGE_OF_ADDRESS_LAMBDA.key]; - const userCase = await applicationContext - .getPersistenceGateway() - .getCaseByDocketNumber({ - applicationContext, - docketNumber, - }); - let caseEntity = new Case(userCase, { applicationContext }); + const jobId = applicationContext.getUniqueId(); - const practitionerName = updatedName || user.name; - const practitionerObject = caseEntity.privatePractitioners - .concat(caseEntity.irsPractitioners) - .find(practitioner => practitioner.userId === user.userId); - - if (!practitionerObject) { - throw new Error( - `Could not find user|${user.userId} barNumber: ${user.barNumber} on ${docketNumber}`, - ); - } - - const oldData = clone(practitionerObject.contact); - - // This updates the case by reference! - practitionerObject.contact = contactInfo; - practitionerObject.firmName = firmName; - practitionerObject.name = practitionerName; - - if (!oldData.email && updatedEmail) { - practitionerObject.serviceIndicator = - SERVICE_INDICATOR_TYPES.SI_ELECTRONIC; - practitionerObject.email = updatedEmail; - } - - // TODO: is this even needed any more? - // we new up another case from the existing case convert '' to null - caseEntity = new Case(caseEntity, { applicationContext }); - - if (!bypassDocketEntry && caseEntity.shouldGenerateNoticesForCase()) { - await prepareToGenerateAndServeDocketEntry({ - applicationContext, - caseEntity, - newData, - oldData, - practitionerName, - user, - }); - } - - const updatedCase = await applicationContext - .getUseCaseHelpers() - .updateCaseAndAssociations({ - applicationContext, - caseToUpdate: caseEntity, - }); - updatedCases.push(updatedCase); - } catch (error) { - applicationContext.logger.error(error); - } - - completedCases++; - await applicationContext.getNotificationGateway().sendNotificationToUser({ - applicationContext, - message: { - action: `${websocketMessagePrefix}_contact_update_progress`, - completedCases, - totalCases: docketNumbers.length, - }, - userId: requestUserId || user.userId, - }); - } - - return updatedCases; -}; - -/** - * This function prepares data to be passed to generateAndServeDocketEntry - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {object} providers.caseEntity the instantiated Case class - * @param {object} providers.newData the new practitioner contact information - * @param {object} providers.oldData the old practitioner contact information (for comparison) - * @param {object} providers.practitionerName the name of the practitioner - * @param {object} providers.user the user object that includes userId, barNumber etc. - * @returns {Promise<*>} resolves upon completion of docket entry service - */ -const prepareToGenerateAndServeDocketEntry = async ({ - applicationContext, - caseEntity, - newData, - oldData, - practitionerName, - user, -}) => { - const documentType = applicationContext - .getUtilities() - .getDocumentTypeForAddressChange({ - newData, - oldData, - }); - - if (!documentType) return; - - const servedParties = aggregatePartiesForService(caseEntity); - - const docketMeta = {} as any; - if (user.role === ROLES.privatePractitioner) { - docketMeta.privatePractitioners = [ - { - name: practitionerName, - }, - ]; - } else if (user.role === ROLES.irsPractitioner) { - docketMeta.partyIrsPractitioner = true; - } - - newData.name = practitionerName; - const { changeOfAddressDocketEntry } = await generateAndServeDocketEntry({ + await applicationContext.getPersistenceGateway().createChangeOfAddressJob({ applicationContext, - barNumber: user.barNumber, - caseEntity, - docketMeta, - documentType, - newData, - oldData, - servedParties, - user, + docketNumbers: associatedUserCases.map(caseInfo => caseInfo.docketNumber), + jobId, }); - caseEntity.updateDocketEntry(changeOfAddressDocketEntry); + applicationContext.logger.info(`creating change of address job of ${jobId}`); + + if (isChangeOfAddressLambdaEnabled) { + const sqs = await applicationContext.getMessagingClient(); + + await Promise.all( + associatedUserCases.map(caseInfo => { + return sqs + .sendMessage({ + MessageBody: JSON.stringify({ + bypassDocketEntry, + contactInfo, + docketNumber: caseInfo.docketNumber, + firmName, + jobId, + requestUser: { + ...applicationContext.getCurrentUser(), + token: undefined, + }, + requestUserId, + updatedEmail, + updatedName, + user, + websocketMessagePrefix, + }), + QueueUrl: `https://sqs.${process.env.REGION}.amazonaws.com/${process.env.AWS_ACCOUNT_ID}/change_of_address_queue_${process.env.STAGE}_${process.env.CURRENT_COLOR}`, + }) + .promise(); + }), + ); + } else { + await Promise.all( + associatedUserCases.map(async caseInfo => { + return await applicationContext + .getUseCaseHelpers() + .generateChangeOfAddressHelper({ + applicationContext, + bypassDocketEntry, + contactInfo, + docketNumber: caseInfo.docketNumber, + firmName, + jobId, + requestUserId, + updatedEmail, + updatedName, + user, + websocketMessagePrefix, + }); + }), + ); + } }; export { generateChangeOfAddressForPractitioner as generateChangeOfAddress }; diff --git a/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.irsPractitioners.test.ts b/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.irsPractitioners.test.ts index 73987406d0d..44eb227fc0c 100644 --- a/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.irsPractitioners.test.ts +++ b/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.irsPractitioners.test.ts @@ -18,19 +18,30 @@ jest.mock('../addCoversheetInteractor', () => ({ describe('generateChangeOfAddress', () => { const { docketNumber } = MOCK_CASE; const mockIrsPractitioner = { + admissionsDate: '2019-04-10', + admissionsStatus: 'Active', barNumber: 'PT5432', + birthYear: '2011', contact: { address1: '234 Main St!', address2: 'Apartment 4', address3: 'Under the stairs', city: 'Chicago', + country: 'USA', countryType: COUNTRY_TYPES.DOMESTIC, phone: '+1 (555) 555-5555', postalCode: '61234', state: 'IL', }, + email: 'irspractitioner1@example.com', + employer: 'IRS', + entityName: 'IrsPractitioner', + firstName: 'rick', + lastName: 'james', name: 'Test IRS Practitioner', + originalBarState: 'FL', + practitionerType: 'Attorney', role: ROLES.irsPractitioner, section: 'irsPractitioner', serviceIndicator: SERVICE_INDICATOR_TYPES.SI_ELECTRONIC, @@ -55,12 +66,16 @@ describe('generateChangeOfAddress', () => { applicationContext .getPersistenceGateway() - .getCasesByUserId.mockReturnValue([{ docketNumber }]); + .getCasesForUser.mockReturnValue([{ docketNumber }]); applicationContext .getPersistenceGateway() .getCaseByDocketNumber.mockReturnValue(mockCaseWithIrsPractitioner); + applicationContext + .getPersistenceGateway() + .setChangeOfAddressCaseAsDone.mockReturnValue({ remaining: 0 }); + applicationContext .getUtilities() .getDocumentTypeForAddressChange.mockReturnValue({ @@ -70,7 +85,7 @@ describe('generateChangeOfAddress', () => { }); it('should run a change of address when address1 changes for an irs practitioner', async () => { - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, bypassDocketEntry: false, contactInfo: { @@ -88,13 +103,15 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getDocumentGenerators().changeOfAddress, ).toHaveBeenCalled(); - expect(cases).toMatchObject([expect.objectContaining({ docketNumber })]); - const changeOfAddressDocketEntry = applicationContext - .getUseCaseHelpers() - .updateCaseAndAssociations.mock.calls[0][0].caseToUpdate.docketEntries.find( - entry => entry.eventCode === 'NCA', - ); - expect(changeOfAddressDocketEntry.partyIrsPractitioner).toEqual(true); + expect( + applicationContext + .getUseCaseHelpers() + .updateCaseAndAssociations.mock.calls[0][0].caseToUpdate.docketEntries.find( + docketEntry => docketEntry.eventCode === 'NCA', + ), + ).toMatchObject({ + partyIrsPractitioner: true, + }); }); it('should not set partyIrsPractitioner if role is not irsPractitioner', async () => { @@ -133,13 +150,13 @@ describe('generateChangeOfAddress', () => { requestUserId: 'abc', updatedEmail: 'new@exaple.com', updatedName: 'rich', - user: {} as any, + user: mockIrsPractitioner as any, websocketMessagePrefix: 'user', }); expect( applicationContext.getNotificationGateway().sendNotificationToUser, - ).toHaveBeenCalledTimes(2); + ).toHaveBeenCalledTimes(3); expect( applicationContext.getNotificationGateway().sendNotificationToUser.mock .calls[0][0].message, @@ -153,15 +170,13 @@ describe('generateChangeOfAddress', () => { .calls[1][0].message, ).toEqual({ action: 'user_contact_update_progress', - completedCases: 1, - totalCases: 1, }); }); it('should NOT send a notification to the user if they have no associated cases', async () => { applicationContext .getPersistenceGateway() - .getCasesByUserId.mockReturnValueOnce([]); + .getCasesForUser.mockReturnValueOnce([]); await generateChangeOfAddress({ applicationContext, @@ -204,19 +219,22 @@ describe('generateChangeOfAddress', () => { websocketMessagePrefix: 'user', }); - const changeOfAddressDocketEntry = applicationContext - .getUseCaseHelpers() - .updateCaseAndAssociations.mock.calls[0][0].caseToUpdate.docketEntries.find( - entry => entry.eventCode === 'NCA', - ); + expect( + applicationContext + .getUseCaseHelpers() + .updateCaseAndAssociations.mock.calls[0][0].caseToUpdate.docketEntries.find( + docketEntry => docketEntry.eventCode === 'NCA', + ), + ).toMatchObject({ + numberOfPages: mockNumberOfPages, + }); expect( applicationContext.getUseCaseHelpers().countPagesInDocument, ).toHaveBeenCalledTimes(1); - expect(changeOfAddressDocketEntry.numberOfPages).toBe(mockNumberOfPages); }); it('should set isAutoGenerated to true on the generated "Notice of Change of Address" document', async () => { - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, bypassDocketEntry: false, contactInfo: { @@ -231,11 +249,14 @@ describe('generateChangeOfAddress', () => { websocketMessagePrefix: 'user', }); - const noticeOfChangeOfAddressDocument = cases[0].docketEntries.find( - d => d.documentType === 'Notice of Change of Address', - ); - - expect(noticeOfChangeOfAddressDocument).toMatchObject({ + expect( + applicationContext + .getUseCaseHelpers() + .updateCaseAndAssociations.mock.calls[0][0].caseToUpdate.docketEntries.find( + docketEntry => + docketEntry.documentTitle === 'Notice of Change of Address', + ), + ).toMatchObject({ isAutoGenerated: true, }); }); diff --git a/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.privatePractitioners.test.ts b/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.privatePractitioners.test.ts index 8cb53f7070c..5090b1b9ce9 100644 --- a/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.privatePractitioners.test.ts +++ b/shared/src/business/useCases/users/generateChangeOfAddressForPractitioner.privatePractitioners.test.ts @@ -19,31 +19,41 @@ jest.mock('../addCoversheetInteractor', () => ({ describe('generateChangeOfAddress', () => { const { docketNumber } = MOCK_CASE; const mockPrivatePractitioner = { + admissionsDate: '2019-04-10', + admissionsStatus: 'Active', barNumber: 'PT5432', + birthYear: '2011', contact: { address1: '234 Main St!', address2: 'Apartment 4', address3: 'Under the stairs', city: 'Chicago', + country: 'USA', countryType: COUNTRY_TYPES.DOMESTIC, phone: '+1 (555) 555-5555', postalCode: '61234', state: 'IL', }, - email: 'privatepractitioner1@example.com', - name: 'Test Private Practitioner', + email: 'irspractitioner1@example.com', + employer: 'IRS', + entityName: 'IrsPractitioner', + firstName: 'rick', + lastName: 'james', + name: 'Test IRS Practitioner', + originalBarState: 'FL', + practitionerType: 'Attorney', role: ROLES.privatePractitioner, - section: 'privatePractitioner', + section: 'irsPractitioner', serviceIndicator: SERVICE_INDICATOR_TYPES.SI_ELECTRONIC, - userId: 'ad07b846-8933-4778-9fe2-b5d8ac8ad728', + userId: '35db9c50-0384-4830-a004-115001e86652', }; const mockCaseWithPrivatePractitioner = { ...MOCK_CASE, privatePractitioners: [mockPrivatePractitioner], status: CASE_STATUS_TYPES.generalDocket, }; - const getDocketEntryForNotice = cases => { - return cases[0].docketEntries.find(entry => + const getDocketEntryForNotice = theCase => { + return theCase.docketEntries.find(entry => entry.documentTitle.includes('Notice of Change'), ); }; @@ -60,7 +70,7 @@ describe('generateChangeOfAddress', () => { applicationContext .getPersistenceGateway() - .getCasesByUserId.mockReturnValue([{ docketNumber }]); + .getCasesForUser.mockReturnValue([{ docketNumber }]); applicationContext .getPersistenceGateway() @@ -72,10 +82,14 @@ describe('generateChangeOfAddress', () => { eventCode: 'NCA', title: 'Notice of Change of Address', }); + + applicationContext + .getPersistenceGateway() + .setChangeOfAddressCaseAsDone.mockReturnValue({ remaining: 0 }); }); it('should run a change of address when address1 changes for a private practitioner', async () => { - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -87,7 +101,12 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getDocumentGenerators().changeOfAddress, ).toHaveBeenCalled(); - expect(cases).toMatchObject([expect.objectContaining({ docketNumber })]); + expect( + applicationContext.getUseCaseHelpers().updateCaseAndAssociations.mock + .calls[0][0].caseToUpdate, + ).toMatchObject({ + docketNumber, + }); }); it('should NOT run a change of address FOR "New" cases when address1 changes for a private practitioner', async () => { @@ -97,7 +116,7 @@ describe('generateChangeOfAddress', () => { ...mockCaseWithPrivatePractitioner, status: CASE_STATUS_TYPES.new, }); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -109,15 +128,12 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getDocumentGenerators().changeOfAddress, ).not.toHaveBeenCalled(); - expect(cases).toMatchObject([ - expect.objectContaining({ docketNumber: MOCK_CASE.docketNumber }), - ]); }); it('should call applicationContext.logger.error and continue processing the next case if the case currently being processed is invalid', async () => { applicationContext .getPersistenceGateway() - .getCasesByUserId.mockReturnValueOnce([ + .getCasesForUser.mockReturnValueOnce([ { ...mockCaseWithPrivatePractitioner, docketNumber: undefined }, mockCaseWithPrivatePractitioner, ]); @@ -129,7 +145,7 @@ describe('generateChangeOfAddress', () => { }) .mockReturnValueOnce(mockCaseWithPrivatePractitioner); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -142,7 +158,6 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getDocumentGenerators().changeOfAddress, ).toHaveBeenCalledTimes(1); - expect(cases).toMatchObject([expect.objectContaining({ docketNumber })]); }); it("should create a work item for an associated practitioner's notice of change of address when paper service is requested by a contact on the case", async () => { @@ -158,7 +173,7 @@ describe('generateChangeOfAddress', () => { ], }); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -167,7 +182,10 @@ describe('generateChangeOfAddress', () => { user: mockPrivatePractitioner, } as any); - const noticeDocketEntry = getDocketEntryForNotice(cases); + const noticeDocketEntry = getDocketEntryForNotice( + applicationContext.getUseCaseHelpers().updateCaseAndAssociations.mock + .calls[0][0].caseToUpdate, + ); expect( applicationContext.getPersistenceGateway().saveWorkItem, @@ -176,7 +194,7 @@ describe('generateChangeOfAddress', () => { }); it("should NOT create a work item for an associated practitioner's notice of change of address when there is no paper service for the case", async () => { - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -185,7 +203,10 @@ describe('generateChangeOfAddress', () => { user: mockPrivatePractitioner, } as any); - const noticeDocketEntry = getDocketEntryForNotice(cases); + const noticeDocketEntry = getDocketEntryForNotice( + applicationContext.getUseCaseHelpers().updateCaseAndAssociations.mock + .calls[0][0].caseToUpdate, + ); expect( applicationContext.getPersistenceGateway().saveWorkItem, @@ -194,7 +215,7 @@ describe('generateChangeOfAddress', () => { }); it('should not create a docket entry, work item, or serve anything if the bypassDocketEntry flag is true', async () => { - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, bypassDocketEntry: true, contactInfo: { @@ -213,9 +234,6 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getPersistenceGateway().saveWorkItem, ).not.toHaveBeenCalled(); - expect(cases).toMatchObject([ - expect.objectContaining({ docketNumber: MOCK_CASE.docketNumber }), - ]); expect( applicationContext.getUseCaseHelpers().updateCaseAndAssociations, ).toHaveBeenCalled(); @@ -230,7 +248,7 @@ describe('generateChangeOfAddress', () => { status: CASE_STATUS_TYPES.closed, }); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -245,9 +263,6 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getPersistenceGateway().saveWorkItem, ).not.toHaveBeenCalled(); - expect(cases).toMatchObject([ - expect.objectContaining({ docketNumber: MOCK_CASE.docketNumber }), - ]); expect( applicationContext.getUseCaseHelpers().updateCaseAndAssociations, ).toHaveBeenCalled(); @@ -265,7 +280,7 @@ describe('generateChangeOfAddress', () => { status: CASE_STATUS_TYPES.closed, }); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -277,7 +292,10 @@ describe('generateChangeOfAddress', () => { }, } as any); - const noticeDocketEntry = getDocketEntryForNotice(cases); + const noticeDocketEntry = getDocketEntryForNotice( + applicationContext.getUseCaseHelpers().updateCaseAndAssociations.mock + .calls[0][0].caseToUpdate, + ); expect( applicationContext.getDocumentGenerators().changeOfAddress, @@ -286,9 +304,6 @@ describe('generateChangeOfAddress', () => { expect( applicationContext.getPersistenceGateway().saveWorkItem, ).toHaveBeenCalled(); - expect(cases).toMatchObject([ - expect.objectContaining({ docketNumber: MOCK_CASE.docketNumber }), - ]); expect( applicationContext.getUseCaseHelpers().updateCaseAndAssociations, ).toHaveBeenCalled(); @@ -299,7 +314,7 @@ describe('generateChangeOfAddress', () => { .getUtilities() .getDocumentTypeForAddressChange.mockReturnValue(undefined); - const cases = await generateChangeOfAddress({ + await generateChangeOfAddress({ applicationContext, contactInfo: { ...mockPrivatePractitioner.contact, @@ -311,7 +326,10 @@ describe('generateChangeOfAddress', () => { }, } as any); - const noticeDocketEntry = getDocketEntryForNotice(cases); + const noticeDocketEntry = getDocketEntryForNotice( + applicationContext.getUseCaseHelpers().updateCaseAndAssociations.mock + .calls[0][0].caseToUpdate, + ); expect( applicationContext.getDocumentGenerators().changeOfAddress, diff --git a/shared/src/business/useCases/users/updateUserContactInformationInteractor.test.ts b/shared/src/business/useCases/users/updateUserContactInformationInteractor.test.ts index 900d9cca420..2dda699f8d6 100644 --- a/shared/src/business/useCases/users/updateUserContactInformationInteractor.test.ts +++ b/shared/src/business/useCases/users/updateUserContactInformationInteractor.test.ts @@ -10,7 +10,6 @@ import { UnauthorizedError } from '../../../../../web-api/src/errors/errors'; import { applicationContext } from '../../test/createTestApplicationContext'; import { entityName as irsPractitionerEntityName } from '../../entities/IrsPractitioner'; import { entityName as practitionerEntityName } from '../../entities/Practitioner'; -import { entityName as privatePractitionerEntityName } from '../../entities/PrivatePractitioner'; import { updateUserContactInformationInteractor } from './updateUserContactInformationInteractor'; jest.mock('./generateChangeOfAddress'); @@ -168,30 +167,6 @@ describe('updateUserContactInformationInteractor', () => { }); }); - it('should update the user when the user being updated is a privatePractitioner', async () => { - mockUser = { - ...mockUser, - entityName: privatePractitionerEntityName, - role: ROLES.privatePractitioner, - }; - - await updateUserContactInformationInteractor(applicationContext, { - contactInfo, - userId: 'f7d90c05-f6cd-442c-a168-202db587f16f', - } as any); - - expect( - applicationContext.getNotificationGateway().sendNotificationToUser.mock - .calls[1][0].message.action, - ).toEqual('user_contact_full_update_complete'); - expect( - applicationContext.getPersistenceGateway().updateUser.mock.calls[1][0] - .user, - ).toMatchObject({ - isUpdatingInformation: false, - }); - }); - it('should update the user when the user being updated is a irsPractitioner', async () => { mockUser = { ...mockUser, @@ -205,38 +180,10 @@ describe('updateUserContactInformationInteractor', () => { } as any); expect( - applicationContext.getNotificationGateway().sendNotificationToUser.mock - .calls[1][0].message.action, - ).toEqual('user_contact_full_update_complete'); - expect( - applicationContext.getPersistenceGateway().updateUser.mock.calls[1][0] - .user, - ).toMatchObject({ - isUpdatingInformation: false, - }); - }); - - it('should update the user when the user being updated is a practitioner', async () => { - mockUser = { - ...mockUser, - entityName: practitionerEntityName, - role: ROLES.privatePractitioner, - }; - - await updateUserContactInformationInteractor(applicationContext, { - contactInfo, - userId: 'f7d90c05-f6cd-442c-a168-202db587f16f', - } as any); - - expect( - applicationContext.getNotificationGateway().sendNotificationToUser.mock - .calls[1][0].message.action, - ).toEqual('user_contact_full_update_complete'); - expect( - applicationContext.getPersistenceGateway().updateUser.mock.calls[1][0] + applicationContext.getPersistenceGateway().updateUser.mock.calls[0][0] .user, ).toMatchObject({ - isUpdatingInformation: false, + isUpdatingInformation: true, }); }); @@ -277,27 +224,6 @@ describe('updateUserContactInformationInteractor', () => { expect(generateChangeOfAddress).toHaveBeenCalled(); }); - it('should notify the user that the update is complete and mark the user as not having an update in progress', async () => { - await updateUserContactInformationInteractor(applicationContext, { - contactInfo, - userId: 'f7d90c05-f6cd-442c-a168-202db587f16f', - } as any); - - expect( - applicationContext.getNotificationGateway().sendNotificationToUser, - ).toHaveBeenCalledTimes(2); - expect( - applicationContext.getNotificationGateway().sendNotificationToUser.mock - .calls[1][0].message.action, - ).toEqual('user_contact_full_update_complete'); - expect( - applicationContext.getPersistenceGateway().updateUser.mock.calls[1][0] - .user, - ).toMatchObject({ - isUpdatingInformation: false, - }); - }); - it('should update the firmName if user is a practitioner and firmName is passed in', async () => { await updateUserContactInformationInteractor(applicationContext, { contactInfo, diff --git a/shared/src/business/useCases/users/updateUserContactInformationInteractor.ts b/shared/src/business/useCases/users/updateUserContactInformationInteractor.ts index 59b29562d7f..1186b710b66 100644 --- a/shared/src/business/useCases/users/updateUserContactInformationInteractor.ts +++ b/shared/src/business/useCases/users/updateUserContactInformationInteractor.ts @@ -98,27 +98,12 @@ const updateUserContactInformationHelper = async ( userId: user.userId, }); - // prevent the progress bar component from showing when updating ONLY the firmName await generateChangeOfAddress({ applicationContext, contactInfo, firmName, user: userEntity.validate().toRawObject(), - }); - - await applicationContext.getNotificationGateway().sendNotificationToUser({ - applicationContext, - message: { - action: 'user_contact_full_update_complete', - }, - userId: user.userId, - }); - - userEntity.isUpdatingInformation = false; - - await applicationContext.getPersistenceGateway().updateUser({ - applicationContext, - user: userEntity.validate().toRawObject(), + websocketMessagePrefix: 'user', }); }; diff --git a/shared/src/business/utilities/documentGenerators/noticeOfReceiptOfPetition.test.ts b/shared/src/business/utilities/documentGenerators/noticeOfReceiptOfPetition.test.ts index 433eaa4af9a..f726bcb8378 100644 --- a/shared/src/business/utilities/documentGenerators/noticeOfReceiptOfPetition.test.ts +++ b/shared/src/business/utilities/documentGenerators/noticeOfReceiptOfPetition.test.ts @@ -31,6 +31,39 @@ describe('noticeOfReceiptOfPetition', () => { 'generates a Notice of Receipt of Petition document with a country included', }); + generateAndVerifyPdfDiff({ + fileName: 'Notice_Receipt_Petition_Long_Address.pdf', + pageNumber: 1, + pdfGenerateFunction: () => { + return noticeOfReceiptOfPetition({ + applicationContext, + data: { + caseCaptionExtension: 'Petitioner(s)', + caseTitle: 'Test Petitioner and A Second Test Petitioner', + contact: { + additionalName: 'Oliver Ellsworth', + address1: '123 Some St.', + address2: 'Building #45', + address3: 'Apartment #56788', + city: 'Jeffersonville', + country: '', + inCareOf: 'John Marshall Harlan', + name: 'Test Petitioner', + postalCode: '12345', + state: 'IN', + title: 'The Esteemed', + }, + docketNumberWithSuffix: '764-23S', + preferredTrialCity: 'Seattle, Washington', + receivedAtFormatted: 'April 12, 2016', + servedDate: 'January 19, 2018', + }, + }); + }, + testDescription: + 'generates a Notice of Receipt of Petition document with a long address label', + }); + generateAndVerifyPdfDiff({ fileName: 'Notice_Receipt_Petition_E_Access.pdf', pageNumber: 1, @@ -61,4 +94,38 @@ describe('noticeOfReceiptOfPetition', () => { testDescription: 'generates a Notice of Receipt of Petition document with dynamic Electronic Access section', }); + + generateAndVerifyPdfDiff({ + fileName: 'Notice_Receipt_Petition_E_Access_Long_Address.pdf', + pageNumber: 1, + pdfGenerateFunction: () => { + return noticeOfReceiptOfPetition({ + applicationContext, + data: { + accessCode: '123456', + caseCaptionExtension: 'Petitioner(s)', + caseTitle: 'Test Petitioner', + contact: { + address1: '123 Some St.', + address2: '456 Bld Avenue', + address3: '789 Street Way', + city: 'Whistler', + country: 'Canada', + hasConsentedToEService: true, + inCareOf: 'Another Person', + name: 'Test Petitioner', + paperPetitionEmail: 'testing@example.com', + postalCode: '80008', + state: 'B.C.', + }, + docketNumberWithSuffix: '123-45S', + preferredTrialCity: 'Birmingham, Alabama', + receivedAtFormatted: 'December 1, 2019', + servedDate: 'June 3, 2020', + }, + }); + }, + testDescription: + 'generates a Notice of Receipt of Petition document with dynamic Electronic Access section for a petitioner with a long address label', + }); }); diff --git a/shared/src/business/utilities/documentGenerators/trialCalendar.test.ts b/shared/src/business/utilities/documentGenerators/trialCalendar.test.ts index c38e5f1b477..2fb5abd0c02 100644 --- a/shared/src/business/utilities/documentGenerators/trialCalendar.test.ts +++ b/shared/src/business/utilities/documentGenerators/trialCalendar.test.ts @@ -29,10 +29,12 @@ describe('trialCalendar', () => { sessionDetail: { address1: '123 Some Street', address2: 'Suite B', - courtReporter: 'Lois Lane', + courtReporter: + 'Lois Lane\n louise.lesley.lane@super_long_email_should_wrap.gov\n Phone: (123) 456-7890', courthouseName: 'Test Courthouse', formattedCityStateZip: 'New York, NY 10108', - irsCalendarAdministrator: 'iCalRS Admin', + irsCalendarAdministrator: + 'Alexandria Ocasio-Cortez\n alexandria.ocasio.cortez@this_email_should_wrap_too.gov \n Phone: (098) 765-4321', judge: 'Joseph Dredd', notes: 'The one with the velour shirt is definitely looking at me funny.', diff --git a/shared/src/business/utilities/htmlGenerator/documents/notice-of-receipt-of-petition.scss b/shared/src/business/utilities/htmlGenerator/documents/notice-of-receipt-of-petition.scss index e1b0466e9f4..f23441f4168 100644 --- a/shared/src/business/utilities/htmlGenerator/documents/notice-of-receipt-of-petition.scss +++ b/shared/src/business/utilities/htmlGenerator/documents/notice-of-receipt-of-petition.scss @@ -1,4 +1,11 @@ #document-notice-of-receipt { + font-size: 14px; + + .info-box { + border: 1px solid #ccc; + margin: 15px 0 20px 0; + } + .court-stamp { position: absolute; right: 0.5in; diff --git a/shared/src/business/utilities/htmlGenerator/documents/trial-calendar.scss b/shared/src/business/utilities/htmlGenerator/documents/trial-calendar.scss index 9361ed36172..547ebc59300 100644 --- a/shared/src/business/utilities/htmlGenerator/documents/trial-calendar.scss +++ b/shared/src/business/utilities/htmlGenerator/documents/trial-calendar.scss @@ -35,4 +35,8 @@ align-items: flex-start; padding: 10px; } + + .wrap-text-content { + word-break: break-word; + } } diff --git a/shared/src/business/utilities/pdfGenerator/documentTemplates/TrialCalendar.tsx b/shared/src/business/utilities/pdfGenerator/documentTemplates/TrialCalendar.tsx index 06acab23184..054323ecdec 100644 --- a/shared/src/business/utilities/pdfGenerator/documentTemplates/TrialCalendar.tsx +++ b/shared/src/business/utilities/pdfGenerator/documentTemplates/TrialCalendar.tsx @@ -60,11 +60,11 @@ export const TrialCalendar = ({ cases = [], sessionDetail }) => {
-
+
Court Reporter
{sessionDetail.courtReporter}
-
+
IRS Calendar Admin
{sessionDetail.irsCalendarAdministrator}
diff --git a/shared/test-pdf-expected-images/Notice_Receipt_Petition.pdf.1.png b/shared/test-pdf-expected-images/Notice_Receipt_Petition.pdf.1.png index 7f16a82a50e..0e28cca542c 100644 Binary files a/shared/test-pdf-expected-images/Notice_Receipt_Petition.pdf.1.png and b/shared/test-pdf-expected-images/Notice_Receipt_Petition.pdf.1.png differ diff --git a/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access.pdf.1.png b/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access.pdf.1.png index 71ff65ef887..778b52beb13 100644 Binary files a/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access.pdf.1.png and b/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access.pdf.1.png differ diff --git a/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access_Long_Address.pdf.1.png b/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access_Long_Address.pdf.1.png new file mode 100644 index 00000000000..bd4a2c6ff2f Binary files /dev/null and b/shared/test-pdf-expected-images/Notice_Receipt_Petition_E_Access_Long_Address.pdf.1.png differ diff --git a/shared/test-pdf-expected-images/Notice_Receipt_Petition_Long_Address.pdf.1.png b/shared/test-pdf-expected-images/Notice_Receipt_Petition_Long_Address.pdf.1.png new file mode 100644 index 00000000000..b0d956f721b Binary files /dev/null and b/shared/test-pdf-expected-images/Notice_Receipt_Petition_Long_Address.pdf.1.png differ diff --git a/shared/test-pdf-expected-images/Trial_Calendar.pdf.1.png b/shared/test-pdf-expected-images/Trial_Calendar.pdf.1.png index 5fe42db5718..43667eaa6bc 100644 Binary files a/shared/test-pdf-expected-images/Trial_Calendar.pdf.1.png and b/shared/test-pdf-expected-images/Trial_Calendar.pdf.1.png differ diff --git a/web-api/src/applicationContext.ts b/web-api/src/applicationContext.ts index 78bd9904c53..88adaeccfcf 100644 --- a/web-api/src/applicationContext.ts +++ b/web-api/src/applicationContext.ts @@ -285,6 +285,9 @@ export const createApplicationContext = ( CASE_INVENTORY_MAX_PAGE_SIZE: 20000, // the Chief Judge will have ~15k records, so setting to 20k to be safe CASE_STATUSES: Object.values(CASE_STATUS_TYPES), + CHANGE_OF_ADDRESS_CONCURRENCY: process.env.CHANGE_OF_ADDRESS_CONCURRENCY + ? parseInt(process.env.CHANGE_OF_ADDRESS_CONCURRENCY) + : undefined, CONFIGURATION_ITEM_KEYS, MAX_SEARCH_CLIENT_RESULTS, MAX_SEARCH_RESULTS, diff --git a/web-api/src/errors/errors.ts b/web-api/src/errors/errors.ts index f905e72832a..ec2f9683350 100644 --- a/web-api/src/errors/errors.ts +++ b/web-api/src/errors/errors.ts @@ -4,6 +4,7 @@ */ export const NotFoundError = class NotFoundError extends Error { public statusCode: number; + public skipLogging?: boolean; /** * constructor * @param {string} message the error message @@ -55,6 +56,8 @@ export const UnknownUserError = class UnknownUserError extends Error { */ export const UnauthorizedError = class UnauthorizedError extends Error { public statusCode: number; + public skipLogging?: boolean; + /** * constructor * @param {string} message the error message diff --git a/web-api/src/getPersistenceGateway.ts b/web-api/src/getPersistenceGateway.ts index b8f18927b1f..93fb8a8944c 100644 --- a/web-api/src/getPersistenceGateway.ts +++ b/web-api/src/getPersistenceGateway.ts @@ -17,6 +17,7 @@ import { confirmAuthCodeLocal } from './persistence/cognito/confirmAuthCodeLocal import { createCase } from './persistence/dynamo/cases/createCase'; import { createCaseDeadline } from './persistence/dynamo/caseDeadlines/createCaseDeadline'; import { createCaseTrialSortMappingRecords } from './persistence/dynamo/cases/createCaseTrialSortMappingRecords'; +import { createChangeOfAddressJob } from './persistence/dynamo/jobs/ChangeOfAddress/createChangeOfAddressJob'; import { createJobStatus } from './persistence/dynamo/trialSessions/createJobStatus'; import { createMessage } from './persistence/dynamo/messages/createMessage'; import { createNewPetitionerUser } from './persistence/dynamo/users/createNewPetitionerUser'; @@ -148,6 +149,7 @@ import { saveDocumentFromLambda } from './persistence/s3/saveDocumentFromLambda' import { saveUserConnection } from './persistence/dynamo/notifications/saveUserConnection'; import { saveWorkItem } from './persistence/dynamo/workitems/saveWorkItem'; import { saveWorkItemForDocketClerkFilingExternalDocument } from './persistence/dynamo/workitems/saveWorkItemForDocketClerkFilingExternalDocument'; +import { setChangeOfAddressCaseAsDone } from './persistence/dynamo/jobs/ChangeOfAddress/setChangeOfAddressCaseAsDone'; import { setMessageAsRead } from './persistence/dynamo/messages/setMessageAsRead'; import { setPriorityOnAllWorkItems } from './persistence/dynamo/workitems/setPriorityOnAllWorkItems'; import { setStoredApplicationHealth } from '@web-api/persistence/dynamo/deployTable/setStoredApplicationHealth'; @@ -291,6 +293,7 @@ const gatewayMethods = { confirmAuthCode: process.env.IS_LOCAL ? confirmAuthCodeLocal : confirmAuthCode, + createChangeOfAddressJob, decrementJobCounter, deleteCaseDeadline, deleteCaseTrialSortMappingRecords, @@ -386,6 +389,7 @@ const gatewayMethods = { : refreshToken, removeIrsPractitionerOnCase, removePrivatePractitionerOnCase, + setChangeOfAddressCaseAsDone, setStoredApplicationHealth, updateUserCaseMapping, verifyCaseForUser, diff --git a/web-api/src/getUseCaseHelpers.ts b/web-api/src/getUseCaseHelpers.ts index 3f2beeb722f..aca629f7ae9 100644 --- a/web-api/src/getUseCaseHelpers.ts +++ b/web-api/src/getUseCaseHelpers.ts @@ -19,6 +19,7 @@ import { fileAndServeDocumentOnOneCase } from '../../shared/src/business/useCase import { formatConsolidatedCaseCoversheetData } from '../../shared/src/business/useCaseHelper/consolidatedCases/formatConsolidatedCaseCoversheetData'; import { generateAndServeDocketEntry } from '../../shared/src/business/useCaseHelper/service/createChangeItems'; import { generateCaseInventoryReportPdf } from '../../shared/src/business/useCaseHelper/caseInventoryReport/generateCaseInventoryReportPdf'; +import { generateChangeOfAddressHelper } from '../../shared/src/business/useCaseHelper/generateChangeOfAddressHelper'; import { generateNoticeOfChangeToInPersonProceeding } from '../../shared/src/business/useCaseHelper/trialSessions/generateNoticeOfChangeToInPersonProceeding'; import { generatePdfFromHtmlHelper } from '../../shared/src/business/useCaseHelper/generatePdfFromHtmlHelper'; import { generateStampedCoversheetInteractor } from '../../shared/src/business/useCaseHelper/stampDisposition/generateStampedCoversheetInteractor'; @@ -65,6 +66,7 @@ const useCaseHelpers = { formatConsolidatedCaseCoversheetData, generateAndServeDocketEntry, generateCaseInventoryReportPdf, + generateChangeOfAddressHelper, generateNoticeOfChangeToInPersonProceeding, generatePdfFromHtmlHelper, generateStampedCoversheetInteractor, diff --git a/web-api/src/persistence/dynamo/caseWorksheet/getCaseWorksheet.ts b/web-api/src/persistence/dynamo/caseWorksheet/getCaseWorksheet.ts index b0c1e19767a..b1c3de55ea6 100644 --- a/web-api/src/persistence/dynamo/caseWorksheet/getCaseWorksheet.ts +++ b/web-api/src/persistence/dynamo/caseWorksheet/getCaseWorksheet.ts @@ -1,3 +1,4 @@ +import { RawCaseWorksheet } from '@shared/business/entities/caseWorksheet/CaseWorksheet'; import { TDynamoRecord } from '@web-api/persistence/dynamo/dynamoTypes'; import { get } from '../../dynamodbClientService'; @@ -7,7 +8,7 @@ export const getCaseWorksheet = async ({ }: { applicationContext: IApplicationContext; docketNumber: string; -}): Promise => { +}): Promise => { return await get({ Key: { pk: `case|${docketNumber}`, diff --git a/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/createChangeOfAddressJob.ts b/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/createChangeOfAddressJob.ts new file mode 100644 index 00000000000..892c2ada929 --- /dev/null +++ b/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/createChangeOfAddressJob.ts @@ -0,0 +1,45 @@ +import { updateConsistent } from '../../../dynamodbClientService'; +import promiseRetry from 'promise-retry'; + +/** + * setTrialSessionProcessingStatus + * + * @param {object} providers the providers object + * @param {object} providers.applicationContext the application context + * @param {object} providers.trialSessionId the trial session id + * @param {object} providers.trialSessionStatus the status of trial session processing + * @returns {Promise} the promise of the call to persistence + */ +export const createChangeOfAddressJob = ({ + applicationContext, + docketNumbers, + jobId, +}: { + applicationContext: IApplicationContext; + jobId: string; + docketNumbers: string[]; +}) => + promiseRetry(retry => + updateConsistent({ + ExpressionAttributeNames: { + '#allCases': 'allCases', + '#jobId': 'jobId', + '#processed': 'processed', + '#remaining': 'remaining', + }, + ExpressionAttributeValues: { + ':jobId': jobId, + ':processed': [], + ':remaining': docketNumbers.length, + ':value': docketNumbers, + }, + Key: { + pk: `change-of-address-job|${jobId}`, + sk: `change-of-address-job|${jobId}`, + }, + ReturnValues: 'UPDATED_NEW', + UpdateExpression: + 'SET #allCases = :value, #remaining = :remaining, #jobId = :jobId, #processed = :processed', + applicationContext, + }).catch(retry), + ); diff --git a/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/setChangeOfAddressCaseAsDone.ts b/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/setChangeOfAddressCaseAsDone.ts new file mode 100644 index 00000000000..9ae06046099 --- /dev/null +++ b/web-api/src/persistence/dynamo/jobs/ChangeOfAddress/setChangeOfAddressCaseAsDone.ts @@ -0,0 +1,41 @@ +import { updateConsistent } from '../../../dynamodbClientService'; +import promiseRetry from 'promise-retry'; + +/** + * setTrialSessionProcessingStatus + * + * @param {object} providers the providers object + * @param {object} providers.applicationContext the application context + * @param {object} providers.trialSessionId the trial session id + * @param {object} providers.trialSessionStatus the status of trial session processing + * @returns {Promise} the promise of the call to persistence + */ +export const setChangeOfAddressCaseAsDone = ({ + applicationContext, + docketNumber, + jobId, +}: { + applicationContext: IApplicationContext; + jobId: string; + docketNumber: string; +}) => + promiseRetry(retry => + updateConsistent({ + ExpressionAttributeNames: { + '#processed': 'processed', + '#remaining': 'remaining', + }, + ExpressionAttributeValues: { + ':decrementValue': -1, + ':value': [docketNumber], + }, + Key: { + pk: `change-of-address-job|${jobId}`, + sk: `change-of-address-job|${jobId}`, + }, + ReturnValues: 'UPDATED_NEW', + UpdateExpression: + 'SET #processed = list_append(#processed, :value) ADD #remaining :decrementValue', + applicationContext, + }).catch(retry), + ); diff --git a/web-api/src/persistence/elasticsearch/getReadyForTrialCases.test.ts b/web-api/src/persistence/elasticsearch/getReadyForTrialCases.test.ts index a63058c2f63..e7e8518fa80 100644 --- a/web-api/src/persistence/elasticsearch/getReadyForTrialCases.test.ts +++ b/web-api/src/persistence/elasticsearch/getReadyForTrialCases.test.ts @@ -1,4 +1,7 @@ -import { CASE_STATUS_TYPES } from '../../../../shared/src/business/entities/EntityConstants'; +import { + ANSWER_DOCUMENT_CODES, + CASE_STATUS_TYPES, +} from '../../../../shared/src/business/entities/EntityConstants'; import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; import { getReadyForTrialCases } from './getReadyForTrialCases'; jest.mock('./searchClient'); @@ -6,7 +9,8 @@ import { search } from './searchClient'; describe('getReadyForTrialCases', () => { it('should search for docket entries of type `Answer` which were served greater than 45 days ago and whose case status is `General Docket - Not at Issue`', async () => { - search.mockResolvedValue({ + const mockSearch = search as jest.Mock; + mockSearch.mockResolvedValue({ results: [{ docketNumber: '102-20' }, { docketNumber: '134-30' }], total: 2, }); @@ -15,7 +19,9 @@ describe('getReadyForTrialCases', () => { applicationContext, }); - expect(search.mock.calls[0][0].searchParameters.body.query).toMatchObject({ + expect( + mockSearch.mock.calls[0][0].searchParameters.body.query, + ).toMatchObject({ bool: { filter: [ { @@ -26,8 +32,8 @@ describe('getReadyForTrialCases', () => { ], must: [ { - term: { - 'documentType.S': 'Answer', + terms: { + 'eventCode.S': ANSWER_DOCUMENT_CODES, }, }, { diff --git a/web-api/src/persistence/elasticsearch/getReadyForTrialCases.ts b/web-api/src/persistence/elasticsearch/getReadyForTrialCases.ts index 16ec69f044f..95a6659b4c7 100644 --- a/web-api/src/persistence/elasticsearch/getReadyForTrialCases.ts +++ b/web-api/src/persistence/elasticsearch/getReadyForTrialCases.ts @@ -1,4 +1,7 @@ -import { CASE_STATUS_TYPES } from '../../../../shared/src/business/entities/EntityConstants'; +import { + ANSWER_DOCUMENT_CODES, + CASE_STATUS_TYPES, +} from '../../../../shared/src/business/entities/EntityConstants'; import { search } from './searchClient'; /** @@ -25,8 +28,8 @@ export const getReadyForTrialCases = async ({ applicationContext }) => { ], must: [ { - term: { - 'documentType.S': 'Answer', + terms: { + 'eventCode.S': ANSWER_DOCUMENT_CODES, }, }, { diff --git a/web-api/terraform/api/change-of-address.tf b/web-api/terraform/api/change-of-address.tf new file mode 100644 index 00000000000..fc5f1a84fd5 --- /dev/null +++ b/web-api/terraform/api/change-of-address.tf @@ -0,0 +1,48 @@ +locals { + timeout_time = "90" +} + +resource "aws_lambda_function" "change_of_address_lambda" { + depends_on = [var.pdf_generation_object] + function_name = "change_of_address_${var.environment}_${var.current_color}" + role = "arn:aws:iam::${var.account_id}:role/lambda_role_${var.environment}" + handler = "pdf-generation.changeOfAddressHandler" + s3_bucket = var.lambda_bucket_id + s3_key = "pdf_generation_${var.current_color}.js.zip" + source_code_hash = var.pdf_generation_object_hash + timeout = local.timeout_time + memory_size = "3008" + + runtime = var.node_version + + environment { + variables = var.lambda_environment + } +} + +resource "aws_lambda_event_source_mapping" "change_of_address_mapping" { + event_source_arn = aws_sqs_queue.change_of_address_queue.arn + function_name = aws_lambda_function.change_of_address_lambda.arn + batch_size = 1 + + scaling_config { + maximum_concurrency = 5 + } + +} + +resource "aws_sqs_queue" "change_of_address_queue" { + name = "change_of_address_queue_${var.environment}_${var.current_color}" + visibility_timeout_seconds = local.timeout_time + + redrive_policy = jsonencode({ + deadLetterTargetArn = aws_sqs_queue.change_of_address_dl_queue.arn + maxReceiveCount = 1 + }) +} + +resource "aws_sqs_queue" "change_of_address_dl_queue" { + name = "change_of_address_dl_queue_${var.environment}_${var.current_color}" +} + + diff --git a/web-api/terraform/api/sqs.tf b/web-api/terraform/api/sqs.tf index b1571732d36..fa3d4de76fb 100644 --- a/web-api/terraform/api/sqs.tf +++ b/web-api/terraform/api/sqs.tf @@ -29,4 +29,3 @@ resource "aws_sqs_queue" "send_emails_dl_queue" { name = "send_emails_dl_queue_${var.environment}_${var.current_color}.fifo" fifo_queue = true } - diff --git a/web-api/terraform/bin/deploy-app.sh b/web-api/terraform/bin/deploy-app.sh index b976c946ac7..e08e4bd7f95 100755 --- a/web-api/terraform/bin/deploy-app.sh +++ b/web-api/terraform/bin/deploy-app.sh @@ -2,6 +2,7 @@ ENV=$1 + DEPLOYING_COLOR=$(../../../scripts/dynamo/get-deploying-color.sh "${ENV}") MIGRATE_FLAG=$(../../../scripts/dynamo/get-migrate-flag.sh "${ENV}") diff --git a/web-api/terraform/main/main.tf b/web-api/terraform/main/main.tf index d2f4d80677a..0cd8cbf032f 100644 --- a/web-api/terraform/main/main.tf +++ b/web-api/terraform/main/main.tf @@ -17,7 +17,7 @@ terraform { } required_providers { - aws = "3.76.0" + aws = "5.15.0" } } diff --git a/web-api/terraform/template/cognito.tf b/web-api/terraform/template/cognito.tf index 6ee50b2016f..3aeb416acad 100644 --- a/web-api/terraform/template/cognito.tf +++ b/web-api/terraform/template/cognito.tf @@ -108,9 +108,17 @@ resource "aws_cognito_user_pool_client" "client" { explicit_auth_flows = ["ADMIN_NO_SRP_AUTH"] generate_secret = false - refresh_token_validity = 30 allowed_oauth_flows_user_pool_client = true + token_validity_units { + access_token = "hours" + id_token = "hours" + refresh_token = "hours" + } + refresh_token_validity = 1 + access_token_validity = 1 + id_token_validity = 1 + callback_urls = [ "http://localhost:1234/log-in", "https://app.${var.dns_domain}/log-in", @@ -241,8 +249,15 @@ resource "aws_cognito_user_pool_client" "irs_client" { explicit_auth_flows = ["ADMIN_NO_SRP_AUTH", "USER_PASSWORD_AUTH"] generate_secret = false - refresh_token_validity = 30 allowed_oauth_flows_user_pool_client = true + token_validity_units { + access_token = "hours" + id_token = "hours" + refresh_token = "hours" + } + refresh_token_validity = 1 + access_token_validity = 1 + id_token_validity = 1 callback_urls = [ "http://localhost:1234/log-in", diff --git a/web-api/terraform/template/lambdas/pdf-generation.js b/web-api/terraform/template/lambdas/pdf-generation.js index 3b12635bd21..23426760ffe 100644 --- a/web-api/terraform/template/lambdas/pdf-generation.js +++ b/web-api/terraform/template/lambdas/pdf-generation.js @@ -22,3 +22,33 @@ export const handler = async event => { return tempId; }; + +/** + * changeOfAddressHandler + * @returns {string} id for the temporary stored pdf + */ +export const changeOfAddressHandler = async event => { + const { Records } = event; + const { body } = Records[0]; + const eventBody = JSON.parse(body); + + const applicationContext = createApplicationContext(eventBody.requestUser); + + applicationContext.logger.info( + `processing job "change-of-address-job|${eventBody.jobId}", task for case ${eventBody.docketNumber}`, + ); + + await applicationContext.getUseCaseHelpers().generateChangeOfAddressHelper({ + applicationContext, + bypassDocketEntry: eventBody.bypassDocketEntry, + contactInfo: eventBody.contactInfo, + docketNumber: eventBody.docketNumber, + firmName: eventBody.firmName, + jobId: eventBody.jobId, + requestUserId: eventBody.requestUserId, + updatedEmail: eventBody.updatedEmail, + updatedName: eventBody.updatedName, + user: eventBody.user, + websocketMessagePrefix: eventBody.websocketMessagePrefix, + }); +}; diff --git a/web-client/integration-tests/journey/practitionerUpdatesAddress.ts b/web-client/integration-tests/journey/practitionerUpdatesAddress.ts index 7825a1601e1..0768a180479 100644 --- a/web-client/integration-tests/journey/practitionerUpdatesAddress.ts +++ b/web-client/integration-tests/journey/practitionerUpdatesAddress.ts @@ -43,12 +43,12 @@ export const practitionerUpdatesAddress = cerebralTest => { refreshInterval: 1000, }); - await waitForLoadingComponentToHide({ cerebralTest }); + await waitForLoadingComponentToHide({ cerebralTest, maxWait: 60000 }); await refreshElasticsearchIndex(5000); expect(cerebralTest.getState('alertSuccess')).toMatchObject({ message: 'Changes saved.', }); - }); + }, 70000); }; diff --git a/web-client/integration-tests/verifyCheckReadyForTrial.test.ts b/web-client/integration-tests/verifyCheckReadyForTrial.test.ts index a9f54ae2334..48caa53e0b4 100644 --- a/web-client/integration-tests/verifyCheckReadyForTrial.test.ts +++ b/web-client/integration-tests/verifyCheckReadyForTrial.test.ts @@ -18,103 +18,123 @@ describe('Invoke checkForReadyForTrialCasesLambda via http request', () => { cerebralTest.closeSocket(); }); - loginAs(cerebralTest, 'petitioner@example.com'); - it('create case', async () => { - const caseDetail = await uploadPetition(cerebralTest); - expect(caseDetail.docketNumber).toBeDefined(); - cerebralTest.docketNumber = caseDetail.docketNumber; - }); - - loginAs(cerebralTest, 'petitionsclerk@example.com'); - petitionsClerkServesPetitionFromDocumentView(cerebralTest); - - loginAs(cerebralTest, 'docketclerk@example.com'); - it('docket clerk creates a paper-filed answer docket entry', async () => { - await cerebralTest.runSequence('gotoAddPaperFilingSequence', { - docketNumber: cerebralTest.docketNumber, + [ + { + documentTitle: 'Answer', + documentType: 'Answer', + eventCode: 'A', + }, + { + documentTitle: 'Answer to Third Amended Petition', + documentType: 'Answer to Third Amended Petition', + eventCode: 'ASAP', + }, + { + documentTitle: 'Answer to Amendment to Petition', + documentType: 'Answer to Amendment to Petition', + eventCode: 'AATP', + }, + ].forEach(document => { + loginAs(cerebralTest, 'petitioner@example.com'); + it('create case', async () => { + const caseDetail = await uploadPetition(cerebralTest); + expect(caseDetail.docketNumber).toBeDefined(); + cerebralTest.docketNumber = caseDetail.docketNumber; }); - const answer = [ - { - key: 'dateReceivedMonth', - value: 1, - }, - { - key: 'dateReceivedDay', - value: 1, - }, - { - key: 'dateReceivedYear', - value: 2018, - }, - { - key: 'eventCode', - value: 'A', - }, - ]; - - for (const item of answer) { + loginAs(cerebralTest, 'petitionsclerk@example.com'); + petitionsClerkServesPetitionFromDocumentView(cerebralTest); + + loginAs(cerebralTest, 'docketclerk@example.com'); + it('docket clerk creates a paper-filed answer docket entry', async () => { + await cerebralTest.runSequence('gotoAddPaperFilingSequence', { + docketNumber: cerebralTest.docketNumber, + }); + + const answer = [ + { + key: 'dateReceivedMonth', + value: 1, + }, + { + key: 'dateReceivedDay', + value: 1, + }, + { + key: 'dateReceivedYear', + value: 2018, + }, + { + key: 'eventCode', + value: document.eventCode, + }, + ]; + + for (const item of answer) { + await cerebralTest.runSequence( + 'updateDocketEntryFormValueSequence', + item, + ); + } + + await cerebralTest.runSequence('setDocumentForUploadSequence', { + documentType: 'primaryDocumentFile', + documentUploadMode: 'preview', + file: fakeFile, + }); + + const { contactId } = contactPrimaryFromState(cerebralTest); await cerebralTest.runSequence( - 'updateDocketEntryFormValueSequence', - item, + 'updateFileDocumentWizardFormValueSequence', + { + key: `filersMap.${contactId}`, + value: true, + }, ); - } - - await cerebralTest.runSequence('setDocumentForUploadSequence', { - documentType: 'primaryDocumentFile', - documentUploadMode: 'preview', - file: fakeFile, - }); - - const { contactId } = contactPrimaryFromState(cerebralTest); - await cerebralTest.runSequence( - 'updateFileDocumentWizardFormValueSequence', - { - key: `filersMap.${contactId}`, - value: true, - }, - ); - - await cerebralTest.runSequence('submitPaperFilingSequence', { - isSavingForLater: false, - }); - await waitForCondition({ - booleanExpressionCondition: () => - cerebralTest.getState('currentPage') === 'CaseDetailInternal', - }); + await cerebralTest.runSequence('submitPaperFilingSequence', { + isSavingForLater: false, + }); + + await waitForCondition({ + booleanExpressionCondition: () => + cerebralTest.getState('currentPage') === 'CaseDetailInternal', + }); + + const caseDocument = cerebralTest.getState('caseDetail.docketEntries.0'); + expect(caseDocument).toMatchObject({ + documentTitle: document.documentTitle, + documentType: document.documentType, + eventCode: document.eventCode, + isFileAttached: true, + }); + expect(cerebralTest.getState('validationErrors')).toEqual({}); + expect(cerebralTest.getState('currentPage')).toEqual( + 'CaseDetailInternal', + ); - const caseDocument = cerebralTest.getState('caseDetail.docketEntries.0'); - expect(caseDocument).toMatchObject({ - documentTitle: 'Answer', - documentType: 'Answer', - eventCode: 'A', - isFileAttached: true, + const caseDetail = cerebralTest.getState('caseDetail'); + expect(caseDetail).toMatchObject({ + status: 'General Docket - Not at Issue', + }); + cerebralTest.docketEntryId = caseDocument.docketEntryId; }); - expect(cerebralTest.getState('validationErrors')).toEqual({}); - expect(cerebralTest.getState('currentPage')).toEqual('CaseDetailInternal'); - const caseDetail = cerebralTest.getState('caseDetail'); - expect(caseDetail).toMatchObject({ - status: 'General Docket - Not at Issue', + it('invoke the lambda', async () => { + await refreshElasticsearchIndex(); + await axios.get('http://localhost:4000/run-check-ready-for-trial'); + await wait(4000); }); - cerebralTest.docketEntryId = caseDocument.docketEntryId; - }); - - it('invoke the lambda', async () => { - await refreshElasticsearchIndex(); - await axios.get('http://localhost:4000/run-check-ready-for-trial'); - await wait(4000); - }); - it('docket clerk verifies that case status is `General Docket - At Issue (Ready for Trial)`', async () => { - await cerebralTest.runSequence('gotoCaseDetailSequence', { - docketNumber: cerebralTest.docketNumber, - }); + it('docket clerk verifies that case status is `General Docket - At Issue (Ready for Trial)`', async () => { + await cerebralTest.runSequence('gotoCaseDetailSequence', { + docketNumber: cerebralTest.docketNumber, + }); - const caseDetail = cerebralTest.getState('caseDetail'); - expect(caseDetail).toMatchObject({ - status: 'General Docket - At Issue (Ready for Trial)', + const caseDetail = cerebralTest.getState('caseDetail'); + expect(caseDetail).toMatchObject({ + status: 'General Docket - At Issue (Ready for Trial)', + }); }); }); }); diff --git a/web-client/src/presenter/actions/CaseWorksheet/updateSubmittedCavCaseDetailAction.ts b/web-client/src/presenter/actions/CaseWorksheet/updateSubmittedCavCaseDetailAction.ts new file mode 100644 index 00000000000..5a67a42ae43 --- /dev/null +++ b/web-client/src/presenter/actions/CaseWorksheet/updateSubmittedCavCaseDetailAction.ts @@ -0,0 +1,31 @@ +import { state } from '@web-client/presenter/app.cerebral'; + +export const updateSubmittedCavCaseDetailAction = async ({ + applicationContext, + get, + props, + store, +}: ActionProps) => { + const { docketNumber, finalBriefDueDate, statusOfMatter } = props; + + store.unset(state.judgeDashboardCaseWorksheetErrors[docketNumber]); + + await applicationContext + .getUseCases() + .updateCaseWorksheetInfoInteractor(applicationContext, { + docketNumber, + finalBriefDueDate, + statusOfMatter, + }); + + const index = get( + state.judgeActivityReportData.submittedAndCavCasesByJudge, + ).findIndex(theCase => theCase.docketNumber === docketNumber); + + if (statusOfMatter !== undefined) { + store.set( + state`judgeActivityReportData.submittedAndCavCasesByJudge.${index}.statusOfMatter`, + statusOfMatter === null ? '' : statusOfMatter, + ); + } +}; diff --git a/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.test.ts b/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.test.ts new file mode 100644 index 00000000000..3e8f387eae1 --- /dev/null +++ b/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.test.ts @@ -0,0 +1,22 @@ +import { MOCK_DOCUMENTS } from '@shared/test/mockDocketEntry'; +import { presenter } from '../../presenter-mock'; +import { runAction } from '@web-client/presenter/test.cerebral'; +import { setDocketRecordOverlayModalStateAction } from './setDocketRecordOverlayModalStateAction'; + +describe('setDocketRecordOverlayModalStateAction', () => { + it('should set state.modal.docketEntry to the entry provided', async () => { + const { state } = await runAction(setDocketRecordOverlayModalStateAction, { + modules: { + presenter, + }, + props: { + entry: MOCK_DOCUMENTS[0], + }, + state: {}, + }); + + expect(state.modal).toEqual({ + docketEntry: MOCK_DOCUMENTS[0], + }); + }); +}); diff --git a/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.ts b/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.ts new file mode 100644 index 00000000000..b1e8b1831d9 --- /dev/null +++ b/web-client/src/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction.ts @@ -0,0 +1,8 @@ +import { state } from '@web-client/presenter/app.cerebral'; + +export const setDocketRecordOverlayModalStateAction = ({ + props, + store, +}: ActionProps) => { + store.set(state.modal.docketEntry, props.entry); +}; diff --git a/web-client/src/presenter/actions/setDocketRecordIndexAction.test.ts b/web-client/src/presenter/actions/setDocketRecordIndexAction.test.ts deleted file mode 100644 index 69f9ece8f33..00000000000 --- a/web-client/src/presenter/actions/setDocketRecordIndexAction.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { runAction } from '@web-client/presenter/test.cerebral'; -import { setDocketRecordIndexAction } from './setDocketRecordIndexAction'; - -describe('setDocketRecordIndexAction', () => { - it('sets state.docketRecordIndex from props', async () => { - const index = 5; - - const { state } = await runAction(setDocketRecordIndexAction, { - props: { - docketRecordIndex: index, - }, - }); - - expect(state.docketRecordIndex).toEqual(index); - }); -}); diff --git a/web-client/src/presenter/actions/setDocketRecordIndexAction.ts b/web-client/src/presenter/actions/setDocketRecordIndexAction.ts deleted file mode 100644 index 8b69fa7d9e2..00000000000 --- a/web-client/src/presenter/actions/setDocketRecordIndexAction.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { state } from '@web-client/presenter/app.cerebral'; - -/** - * sets the state.docketRecordIndex from props.docketRecordIndex - * @param {object} providers the providers object - * @param {object} providers.props the cerebral props object - * @param {object} providers.store the cerebral store - */ -export const setDocketRecordIndexAction = ({ props, store }: ActionProps) => { - store.set(state.docketRecordIndex, props.docketRecordIndex); -}; diff --git a/web-client/src/presenter/actions/setShowModalAction.ts b/web-client/src/presenter/actions/setShowModalAction.ts index c308301e461..7360b4489d8 100644 --- a/web-client/src/presenter/actions/setShowModalAction.ts +++ b/web-client/src/presenter/actions/setShowModalAction.ts @@ -1,11 +1,8 @@ import { state } from '@web-client/presenter/app.cerebral'; -/** - * sets the state.modal.showModal from props - * @param {object} providers the providers object - * @param {object} providers.store the cerebral store used for setting state.users - * @param {object} providers.props the cerebral props object used for getting the props.users - */ -export const setShowModalAction = ({ props, store }: ActionProps) => { +export const setShowModalAction = ({ + props, + store, +}: ActionProps<{ showModal: string }>) => { store.set(state.modal.showModal, props.showModal); }; diff --git a/web-client/src/presenter/actions/userContactUpdateProgressAction.test.ts b/web-client/src/presenter/actions/userContactUpdateProgressAction.test.ts index 755450aeae8..005af8665c1 100644 --- a/web-client/src/presenter/actions/userContactUpdateProgressAction.test.ts +++ b/web-client/src/presenter/actions/userContactUpdateProgressAction.test.ts @@ -12,6 +12,31 @@ describe('userContactUpdateProgressAction', () => { completedCases: 3, totalCases: 15, }, + state: { + userContactEditProgress: {}, + }, + }); + + expect(result.state.userContactEditProgress).toEqual({ + completedCases: 3, + totalCases: 15, + }); + }); + + it('should not set the completed to a lower number on a new event that was lower than the current completedCases count', async () => { + const result = await runAction(userContactUpdateProgressAction, { + modules: { + presenter, + }, + props: { + completedCases: 3, + totalCases: 15, + }, + state: { + userContactEditProgress: { + completedCases: 100, + }, + }, }); expect(result.state.userContactEditProgress).toEqual({ diff --git a/web-client/src/presenter/actions/userContactUpdateProgressAction.ts b/web-client/src/presenter/actions/userContactUpdateProgressAction.ts index 9bd91f4acb9..85baf02d75b 100644 --- a/web-client/src/presenter/actions/userContactUpdateProgressAction.ts +++ b/web-client/src/presenter/actions/userContactUpdateProgressAction.ts @@ -7,11 +7,24 @@ import { state } from '@web-client/presenter/app.cerebral'; * @param {object} providers.store the cerebral store */ export const userContactUpdateProgressAction = ({ + get, props, store, }: ActionProps) => { const { completedCases, totalCases } = props; - store.set(state.userContactEditProgress.totalCases, totalCases); - store.set(state.userContactEditProgress.completedCases, completedCases); + if (totalCases) { + store.set(state.userContactEditProgress.totalCases, totalCases); + } + + if (completedCases !== undefined) { + store.set(state.userContactEditProgress.completedCases, completedCases); + } else { + const currentCompletedCases = + get(state.userContactEditProgress.completedCases) ?? 0; + store.set( + state.userContactEditProgress.completedCases, + currentCompletedCases + 1, + ); + } }; diff --git a/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.test.ts b/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.test.ts index 3fa3c7e8f8a..3afdecb65f9 100644 --- a/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.test.ts +++ b/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.test.ts @@ -2,6 +2,7 @@ import { CASE_STATUS_TYPES, SESSION_TYPES, } from '../../../../../shared/src/business/entities/EntityConstants'; +import { MOCK_SUBMITTED_CASE } from '@shared/test/mockCase'; import { applicationContextForClient as applicationContext } from '@web-client/test/createClientTestApplicationContext'; import { judgeActivityReportHelper as judgeActivityReportHelperComputed } from './judgeActivityReportHelper'; import { judgeUser } from '../../../../../shared/src/test/mockUsers'; @@ -24,15 +25,18 @@ describe('judgeActivityReportHelper', () => { beforeEach(() => { mockSubmittedAndCavCasesByJudge = [ { + ...MOCK_SUBMITTED_CASE, daysElapsedSinceLastStatusChange: 1, docketNumber: '101-20', formattedCaseCount: 4, }, { + ...MOCK_SUBMITTED_CASE, daysElapsedSinceLastStatusChange: 1, docketNumber: '103-20', }, { + ...MOCK_SUBMITTED_CASE, daysElapsedSinceLastStatusChange: 1, docketNumber: '102-20', }, @@ -378,12 +382,14 @@ describe('judgeActivityReportHelper', () => { beforeEach(() => { mockSubmittedAndCavCasesByJudge = [ { + ...MOCK_SUBMITTED_CASE, daysElapsedSinceLastStatusChange: 1, docketNumber: '101-20', formattedCaseCount: 4, leadDocketNumber: '101-20', }, { + ...MOCK_SUBMITTED_CASE, daysElapsedSinceLastStatusChange: 1, docketNumber: '110-15', formattedCaseCount: 1, @@ -452,6 +458,32 @@ describe('judgeActivityReportHelper', () => { submittedAndCavCasesByJudge[2].daysElapsedSinceLastStatusChange, ).toBe(1); }); + + it('should return calculate statusDate using the case caseStatusHistory if available', () => { + const expectedStatusDate = applicationContext + .getUtilities() + .formatDateString( + MOCK_SUBMITTED_CASE.caseStatusHistory[0].date, + applicationContext.getConstants().DATE_FORMATS.MMDDYY, + ); + baseState.judgeActivityReport.judgeActivityReportData.submittedAndCavCasesByJudge = + mockSubmittedAndCavCasesByJudge; + const { submittedAndCavCasesByJudge } = runCompute( + judgeActivityReportHelper, + { + state: baseState, + }, + ); + + expect(submittedAndCavCasesByJudge.length).toBe(3); + expect(submittedAndCavCasesByJudge[0].statusDate).toBe( + expectedStatusDate, + ); + expect(submittedAndCavCasesByJudge[1].statusDate).toBe( + expectedStatusDate, + ); + expect(submittedAndCavCasesByJudge[2].statusDate).toBe(''); + }); }); describe('pageCount and showPaginator', () => { diff --git a/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.ts b/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.ts index e9ec1d04e87..6b5b953cc86 100644 --- a/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.ts +++ b/web-client/src/presenter/computeds/JudgeActivityReport/judgeActivityReportHelper.ts @@ -70,10 +70,12 @@ export const judgeActivityReportHelper = ( individualCase.inConsolidatedGroup = true; } - individualCase.statusDate = getSubmittedOrCAVDate( - applicationContext, - individualCase.caseStatusHistory, - ); + individualCase.statusDate = individualCase.caseStatusHistory + ? getSubmittedOrCAVDate( + applicationContext, + individualCase.caseStatusHistory, + ) + : ''; if (individualCase.caseWorksheet) { individualCase.caseWorksheet.formattedFinalBriefDueDate = individualCase diff --git a/web-client/src/presenter/sequences/showDocketRecordDetailModalSequence.ts b/web-client/src/presenter/sequences/showDocketRecordDetailModalSequence.ts index a1b6be9a3e2..fee2d5988eb 100644 --- a/web-client/src/presenter/sequences/showDocketRecordDetailModalSequence.ts +++ b/web-client/src/presenter/sequences/showDocketRecordDetailModalSequence.ts @@ -1,9 +1,9 @@ import { clearAlertsAction } from '../actions/clearAlertsAction'; -import { setDocketRecordIndexAction } from '../actions/setDocketRecordIndexAction'; +import { setDocketRecordOverlayModalStateAction } from '@web-client/presenter/actions/DocketEntry/setDocketRecordOverlayModalStateAction'; import { setShowModalAction } from '../actions/setShowModalAction'; export const showDocketRecordDetailModalSequence = [ clearAlertsAction, - setDocketRecordIndexAction, + setDocketRecordOverlayModalStateAction, setShowModalAction, ]; diff --git a/web-client/src/presenter/state.ts b/web-client/src/presenter/state.ts index 8b7605fc148..bb058e501ba 100644 --- a/web-client/src/presenter/state.ts +++ b/web-client/src/presenter/state.ts @@ -324,6 +324,8 @@ export const baseState = { timeRemaining: Number.POSITIVE_INFINITY, }, form: {} as any, + + fromPage: '', // shared object for creating new entities, clear before using header: { searchTerm: '', @@ -331,6 +333,7 @@ export const baseState = { showMobileMenu: false, showUsaBannerDetails: false, }, + health: undefined as any, idleStatus: IDLE_STATUS.ACTIVE, idleTimerRef: null, iframeSrc: '', @@ -344,6 +347,7 @@ export const baseState = { messagesInboxCount: 0, messagesSectionCount: 0, modal: { + docketEntry: undefined, pdfPreviewModal: undefined, showModal: undefined, // the name of the modal to display } as Record, @@ -352,7 +356,12 @@ export const baseState = { casesProcessed: 0, totalCases: 0, }, - notifications: {}, + notifications: {} as { + qcSectionInboxCount: number; + qcSectionInProgressCount: number; + qcIndividualInboxCount: number; + qcIndividualInProgressCount: number; + }, openCases: [], paperServiceStatusState: { pdfsAppended: 0, @@ -411,6 +420,7 @@ export const baseState = { sortOrder: ASCENDING, }, trialSession: {} as RawTrialSession, + trialSessionJudge: { name: '', }, diff --git a/web-client/src/ustc-ui/Tabs/Tabs.tsx b/web-client/src/ustc-ui/Tabs/Tabs.tsx index 143c481e0e9..f2bd6eab4a7 100644 --- a/web-client/src/ustc-ui/Tabs/Tabs.tsx +++ b/web-client/src/ustc-ui/Tabs/Tabs.tsx @@ -8,7 +8,7 @@ import { getDefaultAttribute, map } from '../Utils/ElementChildren'; import { props } from 'cerebral'; import { sequences } from '@web-client/presenter/app.cerebral'; import { state } from '@web-client/presenter/app.cerebral'; -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import classNames from 'classnames'; const renderTabFactory = ({ @@ -103,6 +103,19 @@ export function TabsComponent({ onSelect, simpleSetter, value, +}: { + asSwitch?: boolean; + bind?: string; + boxed?: any; + children: ReactNode; + className?: string; + defaultActiveTab?: string; + headingLevel?: string; + id?: string; + marginBottom?: boolean; + onSelect?: any; + simpleSetter?: any; + value?: any; }) { // TODO - Refactor how tab selection sets documentSelectedForScan let activeKey, setTab; diff --git a/web-client/src/views/AdvancedSearch/AdvancedSearch.tsx b/web-client/src/views/AdvancedSearch/AdvancedSearch.tsx index 9f46bae3d46..47d500d7469 100644 --- a/web-client/src/views/AdvancedSearch/AdvancedSearch.tsx +++ b/web-client/src/views/AdvancedSearch/AdvancedSearch.tsx @@ -130,7 +130,7 @@ export const AdvancedSearch = connect(
{(!advancedSearchTab || advancedSearchTab === searchTabs.CASE) && ( diff --git a/web-client/src/views/CaseSearchBox.tsx b/web-client/src/views/CaseSearchBox.tsx index 80c7cad69f6..52e190e7726 100644 --- a/web-client/src/views/CaseSearchBox.tsx +++ b/web-client/src/views/CaseSearchBox.tsx @@ -37,6 +37,7 @@ export const CaseSearchBox = connect( {caseSearchBoxHelper.showAdvancedSearch && ( diff --git a/web-client/src/views/DocketRecord/DocketRecord.tsx b/web-client/src/views/DocketRecord/DocketRecord.tsx index c51ca131431..b234e647eb5 100644 --- a/web-client/src/views/DocketRecord/DocketRecord.tsx +++ b/web-client/src/views/DocketRecord/DocketRecord.tsx @@ -65,7 +65,7 @@ export const DocketRecord = connect( {formattedDocketEntries.formattedDocketEntriesOnDocketRecord.map( - (entry, arrayIndex) => { + entry => { return ( - + {entry.numberOfPages} @@ -137,7 +134,7 @@ export const DocketRecord = connect( - {entry.isLegacySealed && ( + {docketEntry.isLegacySealed && (

)}

Date

-

{entry.createdAtFormatted}

+

{docketEntry.createdAtFormatted}

Pages

-

{entry.numberOfPages}

+

{docketEntry.numberOfPages}

Filed By

-

{entry.filedBy}

+

{docketEntry.filedBy}

Action

-

{entry.action}

+

{docketEntry.action}

Served

- {entry.showNotServed && ( + {docketEntry.showNotServed && ( Not served )} - {entry.showServed && {entry.servedAtFormatted}} + {docketEntry.showServed && ( + {docketEntry.servedAtFormatted} + )}

Parties

-

{entry.servedPartiesCode}

+

{docketEntry.servedPartiesCode}

diff --git a/web-client/src/views/DocketRecord/FilingsAndProceedings.tsx b/web-client/src/views/DocketRecord/FilingsAndProceedings.tsx index a29dd610013..8783f563216 100644 --- a/web-client/src/views/DocketRecord/FilingsAndProceedings.tsx +++ b/web-client/src/views/DocketRecord/FilingsAndProceedings.tsx @@ -8,134 +8,157 @@ import { state } from '@web-client/presenter/app.cerebral'; import React from 'react'; import classNames from 'classnames'; -export const FilingsAndProceedings = connect( - { - arrayIndex: props.arrayIndex, - caseDetail: state.caseDetail, - caseDetailHelper: state.caseDetailHelper, - changeTabAndSetViewerDocumentToDisplaySequence: - sequences.changeTabAndSetViewerDocumentToDisplaySequence, - entry: props.entry, - openCaseDocumentDownloadUrlSequence: - sequences.openCaseDocumentDownloadUrlSequence, - showDocketRecordDetailModalSequence: - sequences.showDocketRecordDetailModalSequence, - }, - function FilingsAndProceedings({ - arrayIndex, - caseDetail, - caseDetailHelper, - changeTabAndSetViewerDocumentToDisplaySequence, - entry, - openCaseDocumentDownloadUrlSequence, - showDocketRecordDetailModalSequence, - }) { - const renderDocumentLink = () => { - return ( - <> - - - - - - - - ); - }; +type FilingsAndProceedingsProps = { + entry: { + descriptionDisplay: string; + isStricken: boolean; + docketEntryId: string; + showDocumentProcessing: boolean; + showLinkToDocument: boolean; + showDocumentViewerLink: boolean; + eventCode: string; + showDocumentDescriptionWithoutLink: boolean; + signatory: string; + isPaper: boolean; + }; +}; - return ( - <> - {entry.showLinkToDocument && renderDocumentLink()} - - {entry.showDocumentProcessing && ( +export const FilingsAndProceedings: React.FunctionComponent = + connect( + { + caseDetail: state.caseDetail, + caseDetailHelper: state.caseDetailHelper, + changeTabAndSetViewerDocumentToDisplaySequence: + sequences.changeTabAndSetViewerDocumentToDisplaySequence, + entry: props.entry, + openCaseDocumentDownloadUrlSequence: + sequences.openCaseDocumentDownloadUrlSequence, + showDocketRecordDetailModalSequence: + sequences.showDocketRecordDetailModalSequence, + }, + function FilingsAndProceedings({ + caseDetail, + caseDetailHelper, + changeTabAndSetViewerDocumentToDisplaySequence, + entry, + openCaseDocumentDownloadUrlSequence, + showDocketRecordDetailModalSequence, + }) { + const renderDocumentLink = () => { + return ( <> - {caseDetailHelper.showDocketRecordInProgressState && ( - - - - )} - - {entry.descriptionDisplay} - + + + + + + - )} + ); + }; - {entry.showDocumentViewerLink && ( - <> - - - )} + + {entry.descriptionDisplay} + + + )} + + {entry.showDocumentViewerLink && ( + <> + + + )} - - {entry.showDocumentDescriptionWithoutLink && entry.descriptionDisplay} - + + {entry.showDocumentDescriptionWithoutLink && + entry.descriptionDisplay} + - {entry.signatory} + {entry.signatory} - {entry.isStricken && (STRICKEN)} - - ); - }, -); + {entry.isStricken && (STRICKEN)} + + ); + }, + ); FilingsAndProceedings.displayName = 'FilingsAndProceedings'; diff --git a/web-client/src/views/TrialSessionWorkingCopy/SessionAssignments.tsx b/web-client/src/views/TrialSessionWorkingCopy/SessionAssignments.tsx index 011ce48c40f..432a17551c5 100644 --- a/web-client/src/views/TrialSessionWorkingCopy/SessionAssignments.tsx +++ b/web-client/src/views/TrialSessionWorkingCopy/SessionAssignments.tsx @@ -38,13 +38,13 @@ export const SessionAssignments = connect( >

Court reporter

-

+

{formattedTrialSessionDetails.formattedCourtReporter}

IRS calendar administrator

-

+

{ formattedTrialSessionDetails.formattedIrsCalendarAdministrator } diff --git a/web-client/src/views/UserContactEditProgress.tsx b/web-client/src/views/UserContactEditProgress.tsx index 01d859ddee3..1fb732fa9c2 100644 --- a/web-client/src/views/UserContactEditProgress.tsx +++ b/web-client/src/views/UserContactEditProgress.tsx @@ -15,7 +15,8 @@ export const UserContactEditProgress = connect(

- Updating contact info in all cases... + Updating contact info in all cases. Please be patient as this + may take awhile.