diff --git a/cypress/local-only/tests/integration/statusReportOrder/status-report-order-description-fields.cy.ts b/cypress/local-only/tests/integration/statusReportOrder/status-report-order-description-fields.cy.ts new file mode 100644 index 00000000000..8b7d96fa55d --- /dev/null +++ b/cypress/local-only/tests/integration/statusReportOrder/status-report-order-description-fields.cy.ts @@ -0,0 +1,112 @@ +import { + FORMATS, + formatNow, +} from '../../../../../shared/src/business/utilities/DateHandler'; +import { + docketNumber, + getLastDraftOrderElementFromDrafts, +} from '../../../support/statusReportOrder'; +import { + loginAsColvin, + loginAsColvinChambers, + loginAsDocketClerk, +} from '../../../../helpers/authentication/login-as-helpers'; +import { logout } from '../../../../helpers/authentication/logout'; + +describe('should default status report order descriptions', () => { + const today = formatNow(FORMATS.MMDDYYYY); + it('should display default description when document type is an Order', () => { + judgeOrChambersCreatesStatusReportOrder(today); + loginAsDocketClerk(); + cy.visit(`/case-detail/${docketNumber}`); + cy.get('#tab-drafts').click(); + getLastDraftOrderElementFromDrafts().click(); + cy.get('[data-testid="add-court-issued-docket-entry-button"]').click(); + cy.get('[data-testid="court-issued-document-type-search"]').should( + 'have.text', + 'Order', + ); + cy.get('[data-testid="document-description-input"]').should( + 'have.value', + `Order parties by ${today} shall file a status report.`, + ); + cy.get('[data-testid="docket-entry-preview-text"]').should( + 'have.text', + `Docket entry preview: Order parties by ${today} shall file a status report.`, + ); + }); + + it('should set event code to OJR when case is stricken from trial session and jurisdiction is retained and display default description', () => { + judgeOrChambersCreatesStatusReportOrder(today, true); + loginAsDocketClerk(); + cy.visit(`/case-detail/${docketNumber}`); + cy.get('#tab-drafts').click(); + getLastDraftOrderElementFromDrafts().click(); + cy.get('[data-testid="add-court-issued-docket-entry-button"]').click(); + cy.get('[data-testid="court-issued-document-type-search"]').should( + 'have.text', + 'Order that jurisdiction is retained', + ); + cy.get('[data-testid="document-description-input"]').should( + 'have.value', + `. Parties by ${today} shall file a status report. Case is stricken from the current trial session.`, + ); + cy.get('[data-testid="judge-select"]').should('have.value', 'Colvin'); + cy.get('[data-testid="docket-entry-preview-text"]').should( + 'have.text', + `Docket entry preview: Order that jurisdiction is retained by Judge Colvin. Parties by ${today} shall file a status report. Case is stricken from the current trial session.`, + ); + }); + + it('should continue to handle OJR and set correct signing judge when status order report is signed by chambers user', () => { + judgeOrChambersCreatesStatusReportOrder(today, true, true); + loginAsDocketClerk(); + cy.visit(`/case-detail/${docketNumber}`); + cy.get('#tab-drafts').click(); + getLastDraftOrderElementFromDrafts().click(); + cy.get('[data-testid="add-court-issued-docket-entry-button"]').click(); + cy.get('[data-testid="court-issued-document-type-search"]').should( + 'have.text', + 'Order that jurisdiction is retained', + ); + cy.get('[data-testid="document-description-input"]').should( + 'have.value', + `. Parties by ${today} shall file a status report. Case is stricken from the current trial session.`, + ); + cy.get('[data-testid="judge-select"]').should('have.value', 'Colvin'); + cy.get('[data-testid="docket-entry-preview-text"]').should( + 'have.text', + `Docket entry preview: Order that jurisdiction is retained by Judge Colvin. Parties by ${today} shall file a status report. Case is stricken from the current trial session.`, + ); + }); +}); + +function judgeOrChambersCreatesStatusReportOrder( + today: string, + jurisdictionRetained: boolean = false, + chambersUser: boolean = false, +) { + if (chambersUser) { + loginAsColvinChambers(); + } else { + loginAsColvin(); + } + cy.visit(`/case-detail/${docketNumber}`); + cy.get('#tab-document-view').click(); + cy.contains('Status Report').click(); + cy.get('[data-testid="status-report-order-button"]').click(); + cy.get('[data-testid="order-type-status-report"]').check({ force: true }); + cy.get('#status-report-due-date-picker').type(today); + + if (jurisdictionRetained) { + cy.get('#stricken-from-trial-sessions-label').click(); + cy.get( + '#jurisdiction-form-group > :nth-child(2) > .usa-radio__label', + ).click(); + cy.get('#jurisdiction-retained').check(); + } + cy.get('[data-testid="save-draft-button"]').click(); + cy.get('[data-testid="sign-pdf-canvas"]').click(); + cy.get('[data-testid="save-signature-button"]').click(); + logout(); +} diff --git a/scripts/env/environments/00-common b/scripts/env/environments/00-common index 03bdbda1827..a562a6e2b3f 100644 --- a/scripts/env/environments/00-common +++ b/scripts/env/environments/00-common @@ -5,7 +5,7 @@ REGION="${DEFAULT_REGION}" CURRENT_COLOR=$(./scripts/dynamo/get-current-color.sh "${ENV}") SOURCE_TABLE=$(./scripts/dynamo/get-source-table.sh "${ENV}") SOURCE_TABLE_VERSION="${SOURCE_TABLE//efcms-${ENV}-/}" -DB_HOST=$(./scripts/postgres/get-host.sh -h) +DB_HOST=$(./scripts/postgres/get-host.sh -h -w) # region hard-coded; all ES domains and Cognito user pools are in us-east-1 ELASTICSEARCH_ENDPOINT=$(aws es describe-elasticsearch-domain \ diff --git a/scripts/run-once-scripts/cleanup-corrupt-messages.ts b/scripts/run-once-scripts/cleanup-corrupt-messages.ts new file mode 100755 index 00000000000..fbaf9e9666a --- /dev/null +++ b/scripts/run-once-scripts/cleanup-corrupt-messages.ts @@ -0,0 +1,244 @@ +#!/usr/bin/env npx ts-node --transpile-only + +import { type ScriptConfig, parseArguments } from '../reports/reportUtils'; +import { + type ServerApplicationContext, + createApplicationContext, +} from '../../web-api/src/applicationContext'; +import { requireEnvVars } from '../../shared/admin-tools/util'; +import { connect } from '../../web-api/src/database'; +import PQueue from 'p-queue'; +import fs from 'fs'; +import { queryFull } from '../../web-api/src/persistence/dynamodbClientService'; +import { Signer } from '@aws-sdk/rds-signer'; +import path from 'path'; +import { Kysely } from 'kysely'; +import { Database } from '../../web-api/src/database-types'; +import { RawCorrespondence } from '../../shared/src/business/entities/Correspondence'; + +requireEnvVars(['REGION', 'DB_NAME', 'DB_HOST', 'DB_USER']); +const { DB_NAME, DB_HOST, DB_USER } = process.env; +const DB_PORT = 5432; + +const scriptConfig: ScriptConfig = { + parameters: { + liveRun: { + required: false, + type: 'boolean', + default: false, + long: 'live-run', + description: + 'If true, will proceed with removing the attachments from the impacted messages.', + }, + }, +}; + +type MessageFragment = { + attachments: any[] | undefined; + docketNumber: string; + messageId: string; +}; + +const getDocketEntryIdsByDocketNumbers = async ({ + applicationContext, + docketNumbers, +}: { + applicationContext: ServerApplicationContext; + docketNumbers: string[]; +}): Promise => { + console.log(`Fetching docket entries for each docket number...`); + + const priorityQueue = new PQueue({ concurrency: 50 }); + + const docketEntryIdsByDocketNumber: Record = {}; + const correspondenceIdsByDocketNumber: Record = {}; + + const getDocketEntriesFunctions = docketNumbers.map( + docketNumber => async () => { + const docketEntries = (await queryFull({ + ExpressionAttributeNames: { + '#pk': 'pk', + '#sk': 'sk', + }, + ExpressionAttributeValues: { + ':pk': `case|${docketNumber}`, + ':prefix': 'docket-entry|', + }, + KeyConditionExpression: '#pk = :pk AND begins_with(#sk, :prefix)', + applicationContext, + })) as RawDocketEntry[]; + + const correspondence = (await queryFull({ + ExpressionAttributeNames: { + '#pk': 'pk', + '#sk': 'sk', + }, + ExpressionAttributeValues: { + ':pk': `case|${docketNumber}`, + ':prefix': 'correspondence|', + }, + KeyConditionExpression: '#pk = :pk AND begins_with(#sk, :prefix)', + applicationContext, + })) as RawCorrespondence[]; + + docketEntryIdsByDocketNumber[docketNumber] = docketEntries.map( + docketEntry => docketEntry.docketEntryId, + ); + + correspondenceIdsByDocketNumber[docketNumber] = correspondence.map( + correspondence => correspondence.correspondenceId, + ); + }, + ); + + await priorityQueue.addAll(getDocketEntriesFunctions); + return { docketEntryIdsByDocketNumber, correspondenceIdsByDocketNumber }; +}; + +const removePoisonAttachmentsFromMessages = async ({ + messageFragments, + docketEntryIdsByDocketNumber, + correspondenceIdsByDocketNumber, +}: { + messageFragments: MessageFragment[]; + docketEntryIdsByDocketNumber: Record; + correspondenceIdsByDocketNumber: Record; +}): Promise<{ + deletedAttachmentAuditRecords: { + messageId: string; + docketEntryId: string; + }[]; + updatedMessageFragments: MessageFragment[]; +}> => { + const updatedMessageFragments: MessageFragment[] = []; + const deletedAttachmentAuditRecords: { + messageId: string; + docketEntryId: string; + }[] = []; + for (const message of messageFragments) { + if (message.attachments) { + for (const attachment of message.attachments) { + if ( + !docketEntryIdsByDocketNumber[message.docketNumber]?.includes( + attachment.documentId, + ) && + !correspondenceIdsByDocketNumber[message.docketNumber]?.includes( + attachment.documentId, + ) + ) { + deletedAttachmentAuditRecords.push({ + messageId: message.messageId, + docketEntryId: attachment.documentId, + }); + console.log( + `Removing attachment ${attachment.documentId} from message ${message.messageId}`, + ); + message.attachments = message.attachments?.filter( + att => att.documentId !== attachment.documentId, + ); + updatedMessageFragments.push(message); + } + } + } + } + + return { + deletedAttachmentAuditRecords, + updatedMessageFragments, + }; +}; + +const udpateMessagesInDb = async ( + db: Kysely, + updatedMessageFragments: MessageFragment[], +) => { + await db.transaction().execute(async trx => { + for (const message of updatedMessageFragments) { + await trx + .updateTable('dwMessage') + .set({ attachments: JSON.stringify(message.attachments) }) + .where('messageId', '=', message.messageId) + .execute(); + } + }); +}; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const applicationContext: ServerApplicationContext = createApplicationContext( + {}, + ); + + const { liveRun } = parseArguments(scriptConfig); + + const sourceSigner = new Signer({ + hostname: DB_HOST!, + port: DB_PORT, + region: 'us-east-1', + username: DB_USER!, + }); + const sourcePassword = await sourceSigner.getAuthToken(); + + const config = { + database: DB_NAME, + host: DB_HOST, + idleTimeoutMillis: 1000, + max: 1, + password: sourcePassword, + port: DB_PORT, + user: DB_USER, + ssl: { + ca: fs.readFileSync('global-bundle.pem').toString(), + }, + }; + + const db = await connect(config); + + console.log('Fetching messages that have not been replied to...'); + const messageFragments = await db + .selectFrom('dwMessage') + .select(['attachments', 'messageId', 'docketNumber']) + .execute(); + + // collect all unique docket numbers from messages + console.log('Collecting unique docket numbers from messages...'); + const docketNumbers = Array.from( + new Set(messageFragments.map(message => message.docketNumber)), + ); + + const { docketEntryIdsByDocketNumber, correspondenceIdsByDocketNumber } = + await getDocketEntryIdsByDocketNumbers({ + applicationContext, + docketNumbers, + }); + + const { deletedAttachmentAuditRecords, updatedMessageFragments } = + await removePoisonAttachmentsFromMessages({ + docketEntryIdsByDocketNumber, + messageFragments, + correspondenceIdsByDocketNumber, + }); + + if (liveRun) { + console.log(`Updating ${updatedMessageFragments.length} messages in DB...`); + await udpateMessagesInDb(db, updatedMessageFragments); + } + + const auditFilename = 'corruptMessageCleanupAudit.json'; + fs.writeFileSync( + auditFilename, + JSON.stringify(deletedAttachmentAuditRecords, null, 2), + ); + + console.log( + '------------------------------------------------------------------', + ); + console.log( + `A log of attachments removed can be found here: ${path.resolve(__dirname, auditFilename)}`, + ); + console.log( + 'Removed attachments count: ', + deletedAttachmentAuditRecords.length, + ); + console.log('Impacted messages count: ', updatedMessageFragments.length); +})(); diff --git a/scripts/run-once-scripts/postgres-migration/batch-delete-dynamo-items.ts b/scripts/run-once-scripts/postgres-migration/batch-delete-dynamo-items.ts new file mode 100644 index 00000000000..60284d20ad4 --- /dev/null +++ b/scripts/run-once-scripts/postgres-migration/batch-delete-dynamo-items.ts @@ -0,0 +1,63 @@ +import { + BatchWriteCommand, + DynamoDBDocumentClient, +} from '@aws-sdk/lib-dynamodb'; + +export async function batchDeleteDynamoItems( + itemsToDelete: { DeleteRequest: { Key: { pk: string; sk: string } } }[], + client: DynamoDBDocumentClient, + tableNameInput: string, +): Promise { + const BATCH_SIZE = 25; + const RETRY_DELAY_MS = 5000; // Set the delay between retries (in milliseconds) + let totalItemsDeleted = 0; + + for (let i = 0; i < itemsToDelete.length; i += BATCH_SIZE) { + const batch = itemsToDelete.slice(i, i + BATCH_SIZE); + + const batchWriteParams = { + RequestItems: { + [tableNameInput]: batch, + }, + }; + + try { + let unprocessedItems: any[] = batch; + let retryCount = 0; + const MAX_RETRIES = 5; + + // Retry logic for unprocessed items + while (unprocessedItems.length > 0 && retryCount < MAX_RETRIES) { + const response = await client.send( + new BatchWriteCommand(batchWriteParams), + ); + + totalItemsDeleted += + unprocessedItems.length - + (response.UnprocessedItems?.[tableNameInput]?.length || 0); + + unprocessedItems = response.UnprocessedItems?.[tableNameInput] ?? []; + + if (unprocessedItems.length > 0) { + console.log( + `Retrying unprocessed items: ${unprocessedItems.length}, attempt ${retryCount + 1}`, + ); + batchWriteParams.RequestItems[tableNameInput] = unprocessedItems; + retryCount++; + + // Add delay before the next retry + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + + if (unprocessedItems.length > 0) { + console.error( + `Failed to delete ${unprocessedItems.length} items after ${MAX_RETRIES} retries.`, + ); + } + } catch (error) { + console.error('Error in batch delete:', error); + } + } + return totalItemsDeleted; +} diff --git a/scripts/run-once-scripts/postgres-migration/delete-case-notes.ts b/scripts/run-once-scripts/postgres-migration/delete-case-notes.ts new file mode 100644 index 00000000000..3fb02442769 --- /dev/null +++ b/scripts/run-once-scripts/postgres-migration/delete-case-notes.ts @@ -0,0 +1,60 @@ +/** + * HOW TO RUN + * npx ts-node --transpileOnly scripts/run-once-scripts/postgres-migration/delete-case-notes.ts + */ + +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { getDbReader } from '../../../web-api/src/database'; +import { isEmpty } from 'lodash'; +import { batchDeleteDynamoItems } from './batch-delete-dynamo-items'; +import { environment } from '../../../web-api/src/environment'; + +const caseUserNotesPageSize = 10000; +const dynamoDbClient = new DynamoDBClient({ region: 'us-east-1' }); +const dynamoDbDocClient = DynamoDBDocumentClient.from(dynamoDbClient); + +// We set the environment as 'production' (= "a deployed environment") to get the RDS connection to work properly +environment.nodeEnv = 'production'; + +const getCaseNotesToDelete = async (offset: number) => { + const caseNotes = await getDbReader(reader => + reader + .selectFrom('dwUserCaseNote') + .select(['docketNumber', 'userId']) + .orderBy(['docketNumber', 'userId']) + .limit(caseUserNotesPageSize) + .offset(offset) + .execute(), + ); + return caseNotes; +}; + +let totalItemsDeleted = 0; + +async function main() { + let offset = 0; + let caseNotesToDelete = await getCaseNotesToDelete(offset); + + while (!isEmpty(caseNotesToDelete)) { + const dynamoItemsToDelete = caseNotesToDelete.map(c => ({ + DeleteRequest: { + Key: { + pk: `user-case-note|${c.docketNumber}`, + sk: `user|${c.userId}`, + }, + }, + })); + totalItemsDeleted += await batchDeleteDynamoItems( + dynamoItemsToDelete, + dynamoDbDocClient, + environment.dynamoDbTableName, + ); + console.log(`Total case notes deleted so far: ${totalItemsDeleted}`); + offset += caseUserNotesPageSize; + caseNotesToDelete = await getCaseNotesToDelete(offset); + } + console.log('Done deleting case notes from Dynamo'); +} + +main().catch(console.error); diff --git a/scripts/run-once-scripts/delete-messages.ts b/scripts/run-once-scripts/postgres-migration/delete-messages.ts similarity index 98% rename from scripts/run-once-scripts/delete-messages.ts rename to scripts/run-once-scripts/postgres-migration/delete-messages.ts index 6ed1a66b356..cc022e06f85 100644 --- a/scripts/run-once-scripts/delete-messages.ts +++ b/scripts/run-once-scripts/postgres-migration/delete-messages.ts @@ -10,7 +10,7 @@ import { ScanCommand, } from '@aws-sdk/lib-dynamodb'; import { DynamoDBClient, ScanCommandInput } from '@aws-sdk/client-dynamodb'; -import { requireEnvVars } from '../../shared/admin-tools/util'; +import { requireEnvVars } from '../../../shared/admin-tools/util'; requireEnvVars(['TABLE_NAME']); diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index 9fb2dae09a2..7ca9895cc1a 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -35,6 +35,8 @@ export const DOCUMENT_EXTERNAL_CATEGORIES_MAP: { } = externalFilingEventsJson; export const COURT_ISSUED_EVENT_CODES = courtIssuedEventCodesJson; +export const EVENT_CODES_THAT_ALLOW_FREE_TEXT = ['O', 'NOT', 'OJR']; + export const DOCKET_NUMBER_MATCHER = /^([1-9]\d{2,4}-\d{2})$/; export const CURRENT_YEAR = +formatNow(FORMATS.YEAR); diff --git a/shared/src/business/entities/factories/UserFactory.test.ts b/shared/src/business/entities/factories/UserFactory.test.ts new file mode 100644 index 00000000000..fcdf2adcc44 --- /dev/null +++ b/shared/src/business/entities/factories/UserFactory.test.ts @@ -0,0 +1,36 @@ +import { Practitioner } from '@shared/business/entities/Practitioner'; +import { ROLES } from '@shared/business/entities/EntityConstants'; +import { User } from '@shared/business/entities/User'; +import { UserFactory } from '@shared/business/entities/factories/UserFactory'; + +describe('UserFactory', () => { + describe('getClass', () => { + it('should return "Practitioner" class type if role is "privatePractitioner"', () => { + const TEST_USER = { role: ROLES.privatePractitioner }; + const userFactory = new UserFactory(TEST_USER); + const classInstance = userFactory.getClass(); + expect(classInstance).toEqual(Practitioner); + }); + + it('should return "Practitioner" class type if role is "irsPractitioner"', () => { + const TEST_USER = { role: ROLES.irsPractitioner }; + const userFactory = new UserFactory(TEST_USER); + const classInstance = userFactory.getClass(); + expect(classInstance).toEqual(Practitioner); + }); + + it('should return "Practitioner" class type if role is "inactivePractitioner"', () => { + const TEST_USER = { role: ROLES.inactivePractitioner }; + const userFactory = new UserFactory(TEST_USER); + const classInstance = userFactory.getClass(); + expect(classInstance).toEqual(Practitioner); + }); + + it('should return "User" class type if role is "admin"', () => { + const TEST_USER = { role: ROLES.admin }; + const userFactory = new UserFactory(TEST_USER); + const classInstance = userFactory.getClass(); + expect(classInstance).toEqual(User); + }); + }); +}); diff --git a/shared/src/business/entities/factories/UserFactory.ts b/shared/src/business/entities/factories/UserFactory.ts new file mode 100644 index 00000000000..7a7617196e6 --- /dev/null +++ b/shared/src/business/entities/factories/UserFactory.ts @@ -0,0 +1,27 @@ +import { Practitioner } from '@shared/business/entities/Practitioner'; +import { ROLES, Role } from '@shared/business/entities/EntityConstants'; +import { User } from '@shared/business/entities/User'; + +type MinimalFactoryInfo = { + role: Role; +}; + +export class UserFactory { + private rawUser: MinimalFactoryInfo; + + constructor(rawUser: MinimalFactoryInfo) { + this.rawUser = rawUser; + } + + public getClass(): typeof User | typeof Practitioner { + if ( + this.rawUser.role === ROLES.privatePractitioner || + this.rawUser.role === ROLES.irsPractitioner || + this.rawUser.role === ROLES.inactivePractitioner + ) { + return Practitioner; + } + + return User; + } +} diff --git a/shared/src/business/utilities/replaceBracketed.ts b/shared/src/business/utilities/replaceBracketed.ts index b7a3b0bfc9f..62d3899b3dc 100644 --- a/shared/src/business/utilities/replaceBracketed.ts +++ b/shared/src/business/utilities/replaceBracketed.ts @@ -6,6 +6,7 @@ export const replaceBracketed = ( while (bracketsMatcher.test(template)) { template = template.replace(bracketsMatcher, values.shift() || ''); } + template = template.replace(/\s+\./g, '.'); template = template.trim(); return template; }; diff --git a/shared/src/proxies/users/verifyUserPendingEmailProxy.ts b/shared/src/proxies/users/verifyUserPendingEmailProxy.ts index 7c0ac503e53..4d42e34b8d4 100644 --- a/shared/src/proxies/users/verifyUserPendingEmailProxy.ts +++ b/shared/src/proxies/users/verifyUserPendingEmailProxy.ts @@ -10,8 +10,8 @@ import { put } from '../requests'; */ export const verifyUserPendingEmailInteractor = ( applicationContext, - { token }, -) => { + { token }: { token: string }, +): Promise => { return put({ applicationContext, body: { diff --git a/types/TEntity.d.ts b/types/TEntity.d.ts index 83013e86ea9..cffd14bcce3 100644 --- a/types/TEntity.d.ts +++ b/types/TEntity.d.ts @@ -27,12 +27,6 @@ type TPetitioner = { hasConsentedToEService?: boolean; }; -type TCaseNote = { - userId: string; - docketNumber: string; - notes: string; -}; - interface IValidateRawCollection { (collection: I[], options: { applicationContext: IApplicationContext }): I[]; } diff --git a/web-api/elasticsearch/efcms-case-mappings.ts b/web-api/elasticsearch/efcms-case-mappings.ts index c637e0c693f..9f381b87937 100644 --- a/web-api/elasticsearch/efcms-case-mappings.ts +++ b/web-api/elasticsearch/efcms-case-mappings.ts @@ -70,6 +70,9 @@ export const efcmsCaseMappings = { 'indexedTimestamp.N': { type: 'text', }, + 'irsPractitioners.L.M.email.S': { + type: 'keyword', + }, 'irsPractitioners.L.M.userId.S': { type: 'keyword', }, @@ -89,6 +92,9 @@ export const efcmsCaseMappings = { 'petitioners.L.M.countryType.S': { type: 'keyword', }, + 'petitioners.L.M.email.S': { + type: 'keyword', + }, 'petitioners.L.M.name.S': { type: 'text', }, @@ -104,6 +110,9 @@ export const efcmsCaseMappings = { 'preferredTrialCity.S': { type: 'keyword', }, + 'privatePractitioners.L.M.email.S': { + type: 'keyword', + }, 'privatePractitioners.L.M.userId.S': { type: 'keyword', }, diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index c1a3d740785..372c6f5f0e4 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -6,7 +6,7 @@ import { UserStatusType, } from '@aws-sdk/client-cognito-identity-provider'; import { MESSAGE_TYPES } from '@web-api/gateways/worker/workerRouter'; -import { MOCK_PRACTITIONER } from '@shared/test/mockUsers'; +import { MOCK_PRACTITIONER, petitionerUser } from '@shared/test/mockUsers'; import { ROLES, Role, @@ -14,7 +14,10 @@ import { } from '../../../../../shared/src/business/entities/EntityConstants'; import { UserRecord } from '@web-api/persistence/dynamo/dynamoTypes'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { changePasswordInteractor } from './changePasswordInteractor'; +import { + changePasswordInteractor, + updateUserPendingEmailRecord, +} from './changePasswordInteractor'; import jwt from 'jsonwebtoken'; describe('changePasswordInteractor', () => { @@ -326,3 +329,45 @@ describe('changePasswordInteractor', () => { }); }); }); + +describe('updateUserPendingEmailRecord', () => { + beforeEach(() => { + applicationContext + .getPersistenceGateway() + .updateUser.mockResolvedValue(null); + }); + + it('should set isUpdatingInformation to true if flag is enabled', async () => { + await updateUserPendingEmailRecord(applicationContext, { + setIsUpdatingInformation: true, + user: { + ...petitionerUser, + isUpdatingInformation: false, + }, + }); + + const updateUserCalls = + applicationContext.getPersistenceGateway().updateUser.mock.calls; + expect(updateUserCalls.length).toEqual(1); + expect(updateUserCalls[0][0].user).toMatchObject({ + isUpdatingInformation: true, + }); + }); + + it('should not update isUpdatingInformation property when flag is disabled', async () => { + await updateUserPendingEmailRecord(applicationContext, { + setIsUpdatingInformation: false, + user: { + ...petitionerUser, + isUpdatingInformation: false, + }, + }); + + const updateUserCalls = + applicationContext.getPersistenceGateway().updateUser.mock.calls; + expect(updateUserCalls.length).toEqual(1); + expect(updateUserCalls[0][0].user).toMatchObject({ + isUpdatingInformation: false, + }); + }); +}); diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index 15a1903edbd..2982c21188d 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -123,7 +123,10 @@ export const changePasswordInteractor = async ( export const updateUserPendingEmailRecord = async ( applicationContext: ServerApplicationContext, - { user }: { user: RawUser }, + { + setIsUpdatingInformation = false, + user, + }: { user: RawUser; setIsUpdatingInformation?: boolean }, ): Promise<{ updatedUser: RawPractitioner | RawUser }> => { let userEntity; @@ -150,6 +153,8 @@ export const updateUserPendingEmailRecord = async ( }); } + if (setIsUpdatingInformation) userEntity.isUpdatingInformation = true; + const rawUser = userEntity.validate().toRawObject(); await applicationContext.getPersistenceGateway().updateUser({ applicationContext, diff --git a/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.test.ts b/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.test.ts index d47eb43b837..38d94ce23f8 100644 --- a/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.test.ts +++ b/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.test.ts @@ -1,13 +1,17 @@ +import '@web-api/persistence/postgres/userCaseNotes/mocks.jest'; import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; import { User } from '../../../../../shared/src/business/entities/User'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { deleteUserCaseNoteInteractor } from './deleteUserCaseNoteInteractor'; +import { deleteUserCaseNote as deleteUserCaseNoteMock } from '@web-api/persistence/postgres/userCaseNotes/deleteUserCaseNote'; import { mockJudgeUser } from '@shared/test/mockAuthUsers'; import { omit } from 'lodash'; describe('deleteUserCaseNoteInteractor', () => { + const deleteUserCaseNote = deleteUserCaseNoteMock as jest.Mock; + it('throws an error if the user is not valid or authorized', async () => { let user = {} as UnknownAuthUser; @@ -33,7 +37,7 @@ describe('deleteUserCaseNoteInteractor', () => { applicationContext .getPersistenceGateway() .getUserById.mockReturnValue(mockUser); - applicationContext.getPersistenceGateway().deleteUserCaseNote = v => v; + deleteUserCaseNote.mockImplementation(v => v); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue({ @@ -60,7 +64,6 @@ describe('deleteUserCaseNoteInteractor', () => { applicationContext .getPersistenceGateway() .getUserById.mockReturnValue(mockUser); - applicationContext.getPersistenceGateway().deleteUserCaseNote = jest.fn(); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(null); @@ -72,9 +75,8 @@ describe('deleteUserCaseNoteInteractor', () => { omit(mockUser, 'section'), ); - expect( - applicationContext.getPersistenceGateway().deleteUserCaseNote.mock - .calls[0][0].userId, - ).toEqual(mockJudgeUser.userId); + expect(deleteUserCaseNote.mock.calls[0][0].userId).toEqual( + mockJudgeUser.userId, + ); }); }); diff --git a/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.ts b/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.ts index 65809d66eaf..4445f8c31a8 100644 --- a/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.ts +++ b/web-api/src/business/useCases/caseNote/deleteUserCaseNoteInteractor.ts @@ -5,6 +5,7 @@ import { import { ServerApplicationContext } from '@web-api/applicationContext'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; +import { deleteUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/deleteUserCaseNote'; /** * deleteUserCaseNoteInteractor @@ -31,8 +32,7 @@ export const deleteUserCaseNoteInteractor = async ( userIdMakingRequest: authorizedUser.userId, }); - return await applicationContext.getPersistenceGateway().deleteUserCaseNote({ - applicationContext, + return await deleteUserCaseNote({ docketNumber, userId, }); diff --git a/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.test.ts b/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.test.ts index 31f054dfec4..7f07f74b45f 100644 --- a/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.test.ts +++ b/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.test.ts @@ -1,8 +1,11 @@ +import '@web-api/persistence/postgres/userCaseNotes/mocks.jest'; import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; +import { UserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { getUserCaseNoteForCasesInteractor } from './getUserCaseNoteForCasesInteractor'; +import { getUserCaseNoteForCases as getUserCaseNoteForCasesMock } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases'; import { mockJudgeUser } from '@shared/test/mockAuthUsers'; import { omit } from 'lodash'; @@ -21,15 +24,15 @@ describe('getUserCaseNoteForCasesInteractor', () => { section: 'colvinChambers', } as UnknownAuthUser; + const getUserCaseNoteForCases = getUserCaseNoteForCasesMock as jest.Mock; + beforeEach(() => { mockCurrentUser = mockJudge; mockNote = MOCK_NOTE; applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => mockCurrentUser); - applicationContext - .getPersistenceGateway() - .getUserCaseNoteForCases.mockResolvedValue([mockNote]); + getUserCaseNoteForCases.mockResolvedValue([new UserCaseNote(mockNote)]); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(mockJudge); @@ -52,9 +55,9 @@ describe('getUserCaseNoteForCasesInteractor', () => { }); it('throws an error if the entity returned from persistence is invalid', async () => { - applicationContext - .getPersistenceGateway() - .getUserCaseNoteForCases.mockResolvedValue([omit(MOCK_NOTE, 'userId')]); + getUserCaseNoteForCases.mockResolvedValue([ + new UserCaseNote([omit(MOCK_NOTE, 'userId')]), + ]); await expect( getUserCaseNoteForCasesInteractor( @@ -100,9 +103,8 @@ describe('getUserCaseNoteForCasesInteractor', () => { omit(mockUser, 'section'), ); - expect( - applicationContext.getPersistenceGateway().getUserCaseNoteForCases.mock - .calls[0][0].userId, - ).toEqual(userIdToExpect); + expect(getUserCaseNoteForCases.mock.calls[0][0].userId).toEqual( + userIdToExpect, + ); }); }); diff --git a/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.ts b/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.ts index 91257ab2877..613b2779468 100644 --- a/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.ts +++ b/web-api/src/business/useCases/caseNote/getUserCaseNoteForCasesInteractor.ts @@ -4,7 +4,7 @@ import { } from '../../../../../shared/src/authorization/authorizationClientService'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; -import { UserCaseNote } from '../../../../../shared/src/business/entities/notes/UserCaseNote'; +import { getUserCaseNoteForCases } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases'; export const getUserCaseNoteForCasesInteractor = async ( applicationContext, @@ -21,13 +21,10 @@ export const getUserCaseNoteForCasesInteractor = async ( userIdMakingRequest: authorizedUser.userId, }); - const caseNotes = await applicationContext - .getPersistenceGateway() - .getUserCaseNoteForCases({ - applicationContext, - docketNumbers, - userId, - }); + const caseNotes = await getUserCaseNoteForCases({ + docketNumbers, + userId, + }); - return caseNotes.map(note => new UserCaseNote(note).validate().toRawObject()); + return caseNotes.map(note => note.validate().toRawObject()); }; diff --git a/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.test.ts b/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.test.ts index c1e25eacca5..4d441df7265 100644 --- a/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.test.ts +++ b/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.test.ts @@ -1,9 +1,11 @@ +import '@web-api/persistence/postgres/userCaseNotes/mocks.jest'; import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; import { User } from '../../../../../shared/src/business/entities/User'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { getUserCaseNoteInteractor } from './getUserCaseNoteInteractor'; +import { getUserCaseNote as getUserCaseNoteMock } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote'; import { mockJudgeUser } from '@shared/test/mockAuthUsers'; import { omit } from 'lodash'; @@ -19,13 +21,13 @@ describe('Get case note', () => { userId: 'unauthorizedUser', } as unknown as UnknownAuthUser; + const getUserCaseNote = getUserCaseNoteMock as jest.Mock; + it('throws error if user is unauthorized', async () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => new User(mockUnauthorizedUser)); - applicationContext - .getPersistenceGateway() - .getUserCaseNote.mockReturnValue({}); + getUserCaseNote.mockReturnValue({}); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(null); @@ -45,9 +47,7 @@ describe('Get case note', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => new User(mockJudgeUser)); - applicationContext - .getPersistenceGateway() - .getUserCaseNote.mockResolvedValue(omit(MOCK_NOTE, 'userId')); + getUserCaseNote.mockResolvedValue(omit(MOCK_NOTE, 'userId')); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(mockJudgeUser); @@ -67,9 +67,7 @@ describe('Get case note', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => new User(mockJudgeUser)); - applicationContext - .getPersistenceGateway() - .getUserCaseNote.mockResolvedValue(MOCK_NOTE); + getUserCaseNote.mockResolvedValue(MOCK_NOTE); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(mockJudgeUser); @@ -89,9 +87,7 @@ describe('Get case note', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => new User(mockJudgeUser)); - applicationContext - .getPersistenceGateway() - .getUserCaseNote.mockResolvedValue(MOCK_NOTE); + getUserCaseNote.mockResolvedValue(MOCK_NOTE); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(null); @@ -114,9 +110,7 @@ describe('Get case note', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => new User(mockJudgeUser)); - applicationContext - .getPersistenceGateway() - .getUserCaseNote.mockReturnValue(null); + getUserCaseNote.mockReturnValue(null); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(mockJudgeUser); diff --git a/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.ts b/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.ts index d7c1c37af7d..17bd2fb6e29 100644 --- a/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.ts +++ b/web-api/src/business/useCases/caseNote/getUserCaseNoteInteractor.ts @@ -6,6 +6,7 @@ import { ServerApplicationContext } from '@web-api/applicationContext'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; import { UserCaseNote } from '../../../../../shared/src/business/entities/notes/UserCaseNote'; +import { getUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote'; /** * getUserCaseNoteInteractor @@ -32,13 +33,10 @@ export const getUserCaseNoteInteractor = async ( userIdMakingRequest: authorizedUser.userId, }); - const caseNote = await applicationContext - .getPersistenceGateway() - .getUserCaseNote({ - applicationContext, - docketNumber, - userId, - }); + const caseNote = await getUserCaseNote({ + docketNumber, + userId, + }); if (caseNote) { return new UserCaseNote(caseNote).validate().toRawObject(); diff --git a/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.test.ts b/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.test.ts index d907227899e..3e1e541f59c 100644 --- a/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.test.ts +++ b/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.test.ts @@ -1,3 +1,4 @@ +import '@web-api/persistence/postgres/userCaseNotes/mocks.jest'; import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; @@ -5,6 +6,7 @@ import { applicationContext } from '../../../../../shared/src/business/test/crea import { mockJudgeUser } from '@shared/test/mockAuthUsers'; import { omit } from 'lodash'; import { updateUserCaseNoteInteractor } from './updateUserCaseNoteInteractor'; +import { upsertUserCaseNote as upsertUserCaseNoteMock } from '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNote'; describe('updateUserCaseNoteInteractor', () => { const mockCaseNote = { @@ -13,6 +15,8 @@ describe('updateUserCaseNoteInteractor', () => { userId: '6805d1ab-18d0-43ec-bafb-654e83405416', }; + const upsertUserCaseNote = upsertUserCaseNoteMock as jest.Mock; + it('throws an error if the user is not valid or authorized', async () => { await expect( updateUserCaseNoteInteractor( @@ -34,9 +38,7 @@ describe('updateUserCaseNoteInteractor', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => mockUser); - applicationContext - .getPersistenceGateway() - .updateUserCaseNote.mockImplementation(v => v.caseNoteToUpdate); + upsertUserCaseNote.mockImplementation(v => v.caseNoteToUpsert); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue({ @@ -67,7 +69,6 @@ describe('updateUserCaseNoteInteractor', () => { applicationContext .getPersistenceGateway() .getUserById.mockImplementation(() => mockUser); - applicationContext.getPersistenceGateway().updateUserCaseNote = jest.fn(); applicationContext .getUseCaseHelpers() .getJudgeInSectionHelper.mockReturnValue(null); @@ -81,9 +82,8 @@ describe('updateUserCaseNoteInteractor', () => { omit(mockUser, 'section'), ); - expect( - applicationContext.getPersistenceGateway().updateUserCaseNote.mock - .calls[0][0].caseNoteToUpdate.userId, - ).toEqual(userIdToExpect); + expect(upsertUserCaseNote.mock.calls[0][0].caseNoteToUpsert.userId).toEqual( + userIdToExpect, + ); }); }); diff --git a/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.ts b/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.ts index b4894a0a629..e7b996fa74e 100644 --- a/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.ts +++ b/web-api/src/business/useCases/caseNote/updateUserCaseNoteInteractor.ts @@ -5,6 +5,7 @@ import { import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; import { UserCaseNote } from '../../../../../shared/src/business/entities/notes/UserCaseNote'; +import { upsertUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNote'; export const updateUserCaseNoteInteractor = async ( applicationContext, @@ -27,14 +28,11 @@ export const updateUserCaseNoteInteractor = async ( docketNumber, notes, userId, - }); - - const caseNoteToUpdate = caseNoteEntity.validate().toRawObject(); + }).validate(); - await applicationContext.getPersistenceGateway().updateUserCaseNote({ - applicationContext, - caseNoteToUpdate, + await upsertUserCaseNote({ + caseNoteToUpsert: caseNoteEntity, }); - return caseNoteToUpdate; + return caseNoteEntity; }; diff --git a/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.test.ts b/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.test.ts index c83d9a05950..b9b79a2ac5b 100644 --- a/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.test.ts +++ b/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.test.ts @@ -8,6 +8,7 @@ import { PARTY_TYPES, PETITIONS_SECTION, ROLES, + STATUS_REPORT_ORDER_OPTIONS, } from '../../../../../shared/src/business/entities/EntityConstants'; import { MOCK_LOCK } from '../../../../../shared/src/test/mockLock'; import { ServiceUnavailableError } from '@web-api/errors/errors'; @@ -21,6 +22,7 @@ import { } from '@shared/test/mockAuthUsers'; import { updateMessage } from '@web-api/persistence/postgres/messages/updateMessage'; +/* eslint-disable max-lines */ describe('fileCourtIssuedOrderInteractor', () => { const mockUserId = applicationContext.getUniqueId(); const caseRecord = { @@ -146,41 +148,6 @@ describe('fileCourtIssuedOrderInteractor', () => { ).toEqual(4); }); - it('should add order document to case and set freeText and draftOrderState.freeText to the document title if it is a generic order (eventCode O)', async () => { - await fileCourtIssuedOrderInteractor( - applicationContext, - { - documentMetadata: { - docketNumber: caseRecord.docketNumber, - documentTitle: 'Order to do anything', - documentType: 'Order', - draftOrderState: {}, - eventCode: 'O', - signedAt: '2019-03-01T21:40:46.415Z', - signedByUserId: mockUserId, - signedJudgeName: 'Dredd', - }, - primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', - }, - mockDocketClerkUser, - ); - - expect( - applicationContext.getPersistenceGateway().getCaseByDocketNumber, - ).toHaveBeenCalled(); - expect( - applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] - .caseToUpdate.docketEntries.length, - ).toEqual(4); - expect( - applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] - .caseToUpdate.docketEntries[3], - ).toMatchObject({ - draftOrderState: { freeText: 'Order to do anything' }, - freeText: 'Order to do anything', - }); - }); - it('should delete draftOrderState properties if they exists on the documentMetadata, after saving the document', async () => { await fileCourtIssuedOrderInteractor( applicationContext, @@ -507,4 +474,347 @@ describe('fileCourtIssuedOrderInteractor', () => { identifiers: [`case|${caseRecord.docketNumber}`], }); }); + + describe('freeText', () => { + describe('eventCode "NOT"', () => { + it('should add order document to case and set freeText and draftOrderState.freeText to the document title if it eventCode NOT', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + docketNumber: caseRecord.docketNumber, + documentTitle: 'Order to do anything', + documentType: 'Order', + draftOrderState: {}, + eventCode: 'NOT', + signedAt: '2019-03-01T21:40:46.415Z', + signedByUserId: mockUserId, + signedJudgeName: 'Dredd', + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries.length, + ).toEqual(4); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { freeText: 'Order to do anything' }, + freeText: 'Order to do anything', + }); + }); + }); + + describe('eventCode "O"', () => { + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.statusReport, + strickenFromTrialSessions: false, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: 'Order parties by 11/05/2024 shall file a status report.', + }, + freeText: 'Order parties by 11/05/2024 shall file a status report.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report and when case is stricken from current trial session', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.statusReport, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + 'Order parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session.', + }, + freeText: + 'Order parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report and when case is stricken from current trial session and jurisdiction is restored to general docket', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + jurisdiction: + STATUS_REPORT_ORDER_OPTIONS.jurisdictionOptions.restored, + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.statusReport, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + 'Order parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session. Case is no longer jurisdiction retained and is restored to the general docket.', + }, + freeText: + 'Order parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session. Case is no longer jurisdiction retained and is restored to the general docket.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report stipulated decision', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.stipulatedDecision, + strickenFromTrialSessions: false, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision.', + }, + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report stipulated decision and when case is stricken from current trial session', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.stipulatedDecision, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session.', + }, + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly for orderType status report stipulated decision and when case is stricken from current trial session and jurisdiction is restored to general docket', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + jurisdiction: + STATUS_REPORT_ORDER_OPTIONS.jurisdictionOptions.restored, + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.stipulatedDecision, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session. Case is no longer jurisdiction retained and is restored to the general docket.', + }, + freeText: + 'Order parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session. Case is no longer jurisdiction retained and is restored to the general docket.', + }); + }); + }); + + describe('eventcode "OJR"', () => { + it('should add order document to case and set freeText and draftOrderState.freeText correctly when order type is statusReport', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + docketNumber: caseRecord.docketNumber, + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + jurisdiction: + STATUS_REPORT_ORDER_OPTIONS.jurisdictionOptions.retained, + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.statusReport, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + '. Parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session.', + }, + freeText: + '. Parties by 11/05/2024 shall file a status report. Case is stricken from the current trial session.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly when order type is statusReportStipulatedDecision and case is stricken from the current trial session', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + docketNumber: caseRecord.docketNumber, + draftOrderState: {}, + dueDate: '2024-11-05', + eventCode: 'O', + jurisdiction: + STATUS_REPORT_ORDER_OPTIONS.jurisdictionOptions.retained, + orderType: + STATUS_REPORT_ORDER_OPTIONS.orderTypeOptions.stipulatedDecision, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: + '. Parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session.', + }, + freeText: + '. Parties by 11/05/2024 shall file a status report or proposed stipulated decision. Case is stricken from the current trial session.', + }); + }); + + it('should add order document to case and set freeText and draftOrderState.freeText correctly when case is stricken from the current trial session', async () => { + await fileCourtIssuedOrderInteractor( + applicationContext, + { + documentMetadata: { + docketNumber: caseRecord.docketNumber, + draftOrderState: {}, + eventCode: 'O', + jurisdiction: + STATUS_REPORT_ORDER_OPTIONS.jurisdictionOptions.retained, + strickenFromTrialSessions: true, + }, + primaryDocumentFileId: 'c54ba5a9-b37b-479d-9201-067ec6e335bb', + }, + mockDocketClerkUser, + ); + expect( + applicationContext.getPersistenceGateway().getCaseByDocketNumber, + ).toHaveBeenCalled(); + expect( + applicationContext.getPersistenceGateway().updateCase.mock.calls[0][0] + .caseToUpdate.docketEntries[3], + ).toMatchObject({ + draftOrderState: { + freeText: '. Case is stricken from the current trial session.', + }, + freeText: '. Case is stricken from the current trial session.', + }); + }); + }); + }); }); diff --git a/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.ts b/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.ts index b7402b20a36..fb552b2ac90 100644 --- a/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.ts +++ b/web-api/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.ts @@ -1,6 +1,14 @@ +import { + COURT_ISSUED_EVENT_CODES, + DOCUMENT_RELATIONSHIPS, + EVENT_CODES_THAT_ALLOW_FREE_TEXT, +} from '../../../../../shared/src/business/entities/EntityConstants'; import { Case } from '../../../../../shared/src/business/entities/cases/Case'; -import { DOCUMENT_RELATIONSHIPS } from '../../../../../shared/src/business/entities/EntityConstants'; import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; +import { + FORMATS, + formatDateString, +} from '@shared/business/utilities/DateHandler'; import { Message } from '../../../../../shared/src/business/entities/Message'; import { ROLE_PERMISSIONS, @@ -40,11 +48,22 @@ export const fileCourtIssuedOrder = async ( }); const caseEntity = new Case(caseToUpdate, { authorizedUser }); - if (['O', 'NOT'].includes(documentMetadata.eventCode)) { - documentMetadata.freeText = documentMetadata.documentTitle; + if ( + documentMetadata.strickenFromTrialSessions && + documentMetadata.jurisdiction === 'retained' + ) { + const ojrEventCode = COURT_ISSUED_EVENT_CODES.find( + e => e.eventCode === 'OJR', + ); + documentMetadata.documentType = ojrEventCode?.documentType; + documentMetadata.eventCode = 'OJR'; + } + + if (EVENT_CODES_THAT_ALLOW_FREE_TEXT.includes(documentMetadata.eventCode)) { + const freeText = generateFreeText(documentMetadata); + documentMetadata.freeText = freeText; if (documentMetadata.draftOrderState) { - documentMetadata.draftOrderState.freeText = - documentMetadata.documentTitle; + documentMetadata.draftOrderState.freeText = freeText; } } @@ -138,3 +157,56 @@ export const fileCourtIssuedOrderInteractor = withLocking( identifiers: [`case|${documentMetadata.docketNumber}`], }), ); + +function generateFreeText(documentMetadata: { + orderType: string; + documentTitle: string; + dueDate: string; + eventCode: string; + strickenFromTrialSessions: boolean; + jurisdiction: string; +}) { + const { + documentTitle, + dueDate, + eventCode, + jurisdiction, + orderType, + strickenFromTrialSessions, + } = documentMetadata; + + const formattedDueDate = formatDateString(dueDate, FORMATS.MMDDYYYY); + if (eventCode === 'OJR') { + return [ + orderType === 'statusReport' && + `. Parties by ${formattedDueDate} shall file a status report.`, + orderType === 'statusReportStipulatedDecision' && + `. Parties by ${formattedDueDate} shall file a status report or proposed stipulated decision.`, + orderType !== 'statusReportStipulatedDecision' && + orderType !== 'statusReport' && + strickenFromTrialSessions && + '.', + strickenFromTrialSessions && + 'Case is stricken from the current trial session.', + ] + .filter(Boolean) + .join(' '); + } + + if (eventCode === 'O' && (orderType || jurisdiction)) { + return [ + 'Order', + orderType === 'statusReport' && + `parties by ${formattedDueDate} shall file a status report.`, + orderType === 'statusReportStipulatedDecision' && + `parties by ${formattedDueDate} shall file a status report or proposed stipulated decision.`, + strickenFromTrialSessions && + 'Case is stricken from the current trial session.', + jurisdiction === 'restoredToGeneralDocket' && + 'Case is no longer jurisdiction retained and is restored to the general docket.', + ] + .filter(Boolean) + .join(' '); + } + return documentTitle; +} diff --git a/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.test.ts b/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.test.ts index 2ee5a87fc45..62a30a45b8e 100644 --- a/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.test.ts +++ b/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.test.ts @@ -5,6 +5,7 @@ jest.mock('./processPractitionerMappingEntries'); jest.mock('./processRemoveEntries'); jest.mock('./processWorkItemEntries'); jest.mock('./processCaseEntries'); +jest.mock('./processUserCaseNoteEntries'); jest.mock('./processOtherEntries'); import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { partitionRecords } from './processStreamUtilities'; @@ -15,6 +16,7 @@ import { processOtherEntries } from './processOtherEntries'; import { processPractitionerMappingEntries } from './processPractitionerMappingEntries'; import { processRemoveEntries } from './processRemoveEntries'; import { processStreamRecordsInteractor } from './processStreamRecordsInteractor'; +import { processUserCaseNoteEntries } from './processUserCaseNoteEntries'; import { processWorkItemEntries } from './processWorkItemEntries'; describe('processStreamRecordsInteractor', () => { @@ -25,6 +27,7 @@ describe('processStreamRecordsInteractor', () => { (processWorkItemEntries as jest.Mock).mockResolvedValue([]); (processMessageEntries as jest.Mock).mockResolvedValue([]); (processPractitionerMappingEntries as jest.Mock).mockResolvedValue([]); + (processUserCaseNoteEntries as jest.Mock).mockResolvedValue([]); (processOtherEntries as jest.Mock).mockResolvedValue([]); (partitionRecords as jest.Mock).mockReturnValue({ @@ -34,6 +37,7 @@ describe('processStreamRecordsInteractor', () => { otherRecords: [], privatePractitionerMappingRecords: [], removeRecords: [], + userCaseNoteRecords: [], workItemRecords: [], }); }); @@ -60,6 +64,7 @@ describe('processStreamRecordsInteractor', () => { expect(processDocketEntries).not.toHaveBeenCalled(); expect(processWorkItemEntries).not.toHaveBeenCalled(); expect(processMessageEntries).not.toHaveBeenCalled(); + expect(processUserCaseNoteEntries).not.toHaveBeenCalled(); expect(processPractitionerMappingEntries).not.toHaveBeenCalled(); expect(processOtherEntries).not.toHaveBeenCalled(); expect(applicationContext.logger.error).toHaveBeenCalledTimes(2); diff --git a/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.ts b/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.ts index 42784a7475e..62bb172816d 100644 --- a/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.ts +++ b/web-api/src/business/useCases/processStreamRecords/processStreamRecordsInteractor.ts @@ -7,6 +7,7 @@ import { processMessageEntries } from './processMessageEntries'; import { processOtherEntries } from './processOtherEntries'; import { processPractitionerMappingEntries } from './processPractitionerMappingEntries'; import { processRemoveEntries } from './processRemoveEntries'; +import { processUserCaseNoteEntries } from '@web-api/business/useCases/processStreamRecords/processUserCaseNoteEntries'; import { processWorkItemEntries } from './processWorkItemEntries'; import type { DynamoDBRecord } from 'aws-lambda'; @@ -22,6 +23,7 @@ export const processStreamRecordsInteractor = async ( otherRecords, practitionerMappingRecords, removeRecords, + userCaseNoteRecords, workItemRecords, } = partitionRecords(recordsToProcess); @@ -75,6 +77,19 @@ export const processStreamRecordsInteractor = async ( throw err; }); + await processUserCaseNoteEntries({ + applicationContext, + userCaseNoteRecords, + }).catch(err => { + applicationContext.logger.error( + 'failed to process userCaseNote records', + { + err, + }, + ); + throw err; + }); + await processPractitionerMappingEntries({ applicationContext, practitionerMappingRecords, diff --git a/web-api/src/business/useCases/processStreamRecords/processStreamUtilities.ts b/web-api/src/business/useCases/processStreamRecords/processStreamUtilities.ts index 16efeb1abb5..1b5cb1db0b1 100644 --- a/web-api/src/business/useCases/processStreamRecords/processStreamUtilities.ts +++ b/web-api/src/business/useCases/processStreamRecords/processStreamUtilities.ts @@ -48,8 +48,15 @@ export const partitionRecords = ( record.dynamodb.NewImage.entityName.S === 'Message', ); - const [completionMarkers, otherRecords] = partition( + const [userCaseNoteRecords, nonUserCaseNoteRecords] = partition( nonMessageRecords, + record => + record.dynamodb?.NewImage?.entityName && + record.dynamodb.NewImage.entityName.S === 'UserCaseNote', + ); + + const [completionMarkers, otherRecords] = partition( + nonUserCaseNoteRecords, record => record.dynamodb?.NewImage?.entityName && record.dynamodb.NewImage.entityName.S === 'CompletionMarker', @@ -63,6 +70,7 @@ export const partitionRecords = ( otherRecords, practitionerMappingRecords, removeRecords, + userCaseNoteRecords, workItemRecords, }; }; diff --git a/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.test.ts b/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.test.ts new file mode 100644 index 00000000000..b62ef80ae62 --- /dev/null +++ b/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.test.ts @@ -0,0 +1,44 @@ +import '@web-api/persistence/postgres/userCaseNotes/mocks.jest'; +import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; +import { processUserCaseNoteEntries } from '@web-api/business/useCases/processStreamRecords/processUserCaseNoteEntries'; +import { upsertUserCaseNotes } from '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNotes'; + +describe('processUserCaseNoteEntries', () => { + beforeEach(() => { + (upsertUserCaseNotes as jest.Mock).mockResolvedValue(undefined); + }); + + it('should attempt to store the user case notes using the upsert method', async () => { + const mockUserCaseNoteRecord = { + dynamodb: { + NewImage: { + docketNumber: { + S: '104-20', + }, + entityName: { + S: 'UserCaseNote', + }, + notes: { + S: 'Test', + }, + pk: { + S: 'user-case-note|104-20', + }, + sk: { + S: 'user|c4a1a9da-ac90-40f1-8d4d-d494c219cbbe', + }, + userId: { + S: 'c4a1a9da-ac90-40f1-8d4d-d494c219cbbe', + }, + }, + }, + }; + + await processUserCaseNoteEntries({ + applicationContext, + userCaseNoteRecords: [mockUserCaseNoteRecord], + }); + + expect(upsertUserCaseNotes).toHaveBeenCalled(); + }); +}); diff --git a/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.ts b/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.ts new file mode 100644 index 00000000000..7cc15c27528 --- /dev/null +++ b/web-api/src/business/useCases/processStreamRecords/processUserCaseNoteEntries.ts @@ -0,0 +1,26 @@ +import { RawUserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; +import { unmarshall } from '@aws-sdk/util-dynamodb'; +import { upsertUserCaseNotes } from '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNotes'; +import type { ServerApplicationContext } from '@web-api/applicationContext'; + +export const processUserCaseNoteEntries = async ({ + applicationContext, + userCaseNoteRecords, +}: { + applicationContext: ServerApplicationContext; + userCaseNoteRecords: any[]; +}) => { + if (!userCaseNoteRecords.length) return; + + applicationContext.logger.debug( + `going to index ${userCaseNoteRecords.length} userCaseNote records`, + ); + + await upsertUserCaseNotes( + userCaseNoteRecords.map(userCaseNoteRecord => { + return unmarshall( + userCaseNoteRecord.dynamodb.NewImage, + ) as RawUserCaseNote; + }), + ); +}; diff --git a/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.test.ts b/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.test.ts new file mode 100644 index 00000000000..15e3e5137a1 --- /dev/null +++ b/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.test.ts @@ -0,0 +1,179 @@ +import { + MAX_ITERATIONS, + queueEmailUpdateAssociatedCasesWorker, +} from '@web-api/business/useCases/user/queueEmailUpdateAssociatedCasesWorker'; +import { RawUser } from '@shared/business/entities/User'; +import { applicationContext } from '@shared/business/test/createTestApplicationContext'; +import { mockPetitionerUser } from '@shared/test/mockAuthUsers'; +import { petitionerUser } from '@shared/test/mockUsers'; +import { sleep } from '@shared/tools/helpers'; + +describe('queueEmailUpdateAssociatedCasesWorker', () => { + let TEST_USER: RawUser; + let RESOLVER: Function; + + beforeEach(() => { + TEST_USER = { + ...petitionerUser, + isUpdatingInformation: true, + }; + + applicationContext.getPersistenceGateway().updateUser.mockReturnValue(null); + + applicationContext + .getUseCases() + .queueUpdateAssociatedCasesWorker.mockReturnValue(null); + + applicationContext + .getPersistenceGateway() + .getCasesByEmailTotal.mockImplementation( + () => + new Promise(resolve => { + RESOLVER = resolve; + }), + ); + + applicationContext.getUtilities().sleep.mockImplementation(() => {}); + }); + + it('should disable user flag and short circuit if there is no associated cases to user', async () => { + applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser.mockReturnValue([]); + + await queueEmailUpdateAssociatedCasesWorker( + applicationContext, + { user: TEST_USER }, + mockPetitionerUser, + ); + + const updateUserCalls = + applicationContext.getPersistenceGateway().updateUser.mock.calls; + expect(updateUserCalls.length).toEqual(1); + expect(updateUserCalls[0][0].user).toMatchObject({ + isUpdatingInformation: false, + }); + + const queueUpdateAssociatedCasesWorkerCalls = + applicationContext.getUseCases().queueUpdateAssociatedCasesWorker.mock + .calls; + expect(queueUpdateAssociatedCasesWorkerCalls.length).toEqual(0); + }); + + function assertFunctionCalls(expectedCount: number) { + expect( + applicationContext.getPersistenceGateway().getDocketNumbersByUser.mock + .calls.length, + ).toEqual(expectedCount + 1); + + expect( + applicationContext.getPersistenceGateway().getCasesByEmailTotal.mock.calls + .length, + ).toEqual(expectedCount); + } + + it('should call "queueUpdateAssociatedCasesWorker" with user information and wait until all expected cases to update', async () => { + const TEST_DOCKER_NUMBERS = ['TEST_1', 'TEST_2', 'TEST_3', 'TEST_4']; + let COMPLETE_FLAG = false; + applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser.mockReturnValue(TEST_DOCKER_NUMBERS); + + void queueEmailUpdateAssociatedCasesWorker( + applicationContext, + { user: TEST_USER }, + mockPetitionerUser, + ).then(() => { + const queueUpdateAssociatedCasesWorkerCalls = + applicationContext.getUseCases().queueUpdateAssociatedCasesWorker.mock + .calls; + + expect(queueUpdateAssociatedCasesWorkerCalls.length).toEqual(1); + expect(queueUpdateAssociatedCasesWorkerCalls[0][1]).toEqual({ + user: TEST_USER, + }); + expect(queueUpdateAssociatedCasesWorkerCalls[0][2]).toEqual( + mockPetitionerUser, + ); + + const updateUserCalls = + applicationContext.getPersistenceGateway().updateUser.mock.calls; + expect(updateUserCalls.length).toEqual(1); + expect(updateUserCalls[0][0].user).toMatchObject({ + isUpdatingInformation: false, + }); + + COMPLETE_FLAG = true; + }); + + await sleep(100); + assertFunctionCalls(1); + RESOLVER(0); + + await sleep(50); + assertFunctionCalls(2); + RESOLVER(2); + + await sleep(50); + assertFunctionCalls(3); + RESOLVER(TEST_DOCKER_NUMBERS.length); + + await sleep(50); + expect(COMPLETE_FLAG).toEqual(true); + }); + + it('should call resolve the interactor when the max number of iterations is met', async () => { + applicationContext + .getPersistenceGateway() + .getCasesByEmailTotal.mockImplementation(() => {}); + + const TEST_DOCKER_NUMBERS = ['TEST_1', 'TEST_2', 'TEST_3', 'TEST_4']; + let COMPLETE_FLAG = false; + applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser.mockReturnValue(TEST_DOCKER_NUMBERS); + + void queueEmailUpdateAssociatedCasesWorker( + applicationContext, + { user: TEST_USER }, + mockPetitionerUser, + ).then(() => { + COMPLETE_FLAG = true; + }); + + await sleep(100); + expect(COMPLETE_FLAG).toEqual(true); + + const getCasesByEmailTotalCalls = + applicationContext.getPersistenceGateway().getCasesByEmailTotal.mock + .calls; + + expect(getCasesByEmailTotalCalls.length).toEqual(MAX_ITERATIONS + 1); + }); + + it('should resolve the interactor when the there is an error thrown in the check method', async () => { + applicationContext + .getPersistenceGateway() + .getCasesByEmailTotal.mockImplementation(() => { + throw Error('TEST ERROR'); + }); + + const TEST_DOCKER_NUMBERS = ['TEST_1', 'TEST_2', 'TEST_3', 'TEST_4']; + applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser.mockReturnValue(TEST_DOCKER_NUMBERS); + + await queueEmailUpdateAssociatedCasesWorker( + applicationContext, + { user: TEST_USER }, + mockPetitionerUser, + ); + + const updateUserCalls = + applicationContext.getPersistenceGateway().updateUser.mock.calls; + expect(updateUserCalls.length).toEqual(1); + expect(updateUserCalls[0][0].user).toMatchObject({ + isUpdatingInformation: false, + }); + }); +}); diff --git a/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.ts b/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.ts new file mode 100644 index 00000000000..7c9f53cf845 --- /dev/null +++ b/web-api/src/business/useCases/user/queueEmailUpdateAssociatedCasesWorker.ts @@ -0,0 +1,102 @@ +import { AuthUser } from '@shared/business/entities/authUser/AuthUser'; +import { RawPractitioner } from '@shared/business/entities/Practitioner'; +import { RawUser } from '@shared/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; +import { UserFactory } from '@shared/business/entities/factories/UserFactory'; + +async function disableIsUserUpdatingFlag({ + applicationContext, + user, +}: { + applicationContext: ServerApplicationContext; + user: RawUser | RawPractitioner; +}): Promise { + const userFactory = new UserFactory(user); + const UserClass = userFactory.getClass(); + + user.isUpdatingInformation = false; + const userEntity = new UserClass(user); + + await applicationContext.getPersistenceGateway().updateUser({ + applicationContext, + user: userEntity.validate().toRawObject(), + }); +} + +export const queueEmailUpdateAssociatedCasesWorker = async ( + applicationContext: ServerApplicationContext, + { user }: { user: RawUser | RawPractitioner }, + authorizedUser: AuthUser, +): Promise => { + const docketNumbersByUser = await applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser({ + applicationContext, + userId: user.userId, + }); + + if (!docketNumbersByUser.length) { + await disableIsUserUpdatingFlag({ applicationContext, user }); + return; + } + + await applicationContext + .getUseCases() + .queueUpdateAssociatedCasesWorker( + applicationContext, + { user }, + authorizedUser, + ); + + await waitUntilAllExpectedCasesAreUpdatedWithEmail({ + applicationContext, + userEmail: user.email!, + }) + .catch(error => + console.error(`ERROR CHECKING COUNT OF UPDATED CASES -> ${error}`), + ) + .finally(async () => { + await disableIsUserUpdatingFlag({ applicationContext, user }); + }); +}; + +const WAIT_TIMEOUT = 2000; +const MAX_WAITTIME_IN_MINUTES = 14; +export const MAX_ITERATIONS = Math.floor( + (MAX_WAITTIME_IN_MINUTES * 60 * 1000) / WAIT_TIMEOUT, +); + +async function waitUntilAllExpectedCasesAreUpdatedWithEmail({ + applicationContext, + iteration = 0, + userEmail, +}: { + applicationContext: ServerApplicationContext; + iteration?: number; + userEmail: string; +}): Promise { + await applicationContext.getUtilities().sleep(WAIT_TIMEOUT); + + const docketNumbersByUser = await applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser({ + applicationContext, + userId: userEmail, + }); + const expectedCount = docketNumbersByUser.length; + + const actualCount = await applicationContext + .getPersistenceGateway() + .getCasesByEmailTotal({ + applicationContext, + email: userEmail, + }); + + if (actualCount >= expectedCount) return; + if (iteration >= MAX_ITERATIONS) return; + return waitUntilAllExpectedCasesAreUpdatedWithEmail({ + applicationContext, + iteration: iteration + 1, + userEmail, + }); +} diff --git a/web-api/src/business/useCases/user/updateUserContactInformationInteractor.locking.test.ts b/web-api/src/business/useCases/user/updateUserContactInformationInteractor.locking.test.ts index 337e76034d4..e0ebab46a55 100644 --- a/web-api/src/business/useCases/user/updateUserContactInformationInteractor.locking.test.ts +++ b/web-api/src/business/useCases/user/updateUserContactInformationInteractor.locking.test.ts @@ -10,6 +10,7 @@ import { handleLockError, updateUserContactInformationInteractor, } from './updateUserContactInformationInteractor'; +import { sleep } from '@shared/tools/helpers'; const contactInfo = { address1: '234 Main St', @@ -37,6 +38,10 @@ describe('determineEntitiesToLock', () => { applicationContext .getPersistenceGateway() .getCasesForUser.mockReturnValue(mockCases); + + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockResolvedValue(null); }); it('should lookup the docket numbers for the specified user', async () => { @@ -58,6 +63,34 @@ describe('determineEntitiesToLock', () => { expect(identifiers).toContain(`case|${mockCase.docketNumber}`); }); }); + + it('should wait until user is free before calling getCasesForUser', async () => { + let resolver: Function; + + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockImplementation(() => { + return new Promise(resolve => (resolver = resolve)); + }); + + void determineEntitiesToLock(applicationContext, mockParams); + + await sleep(50); + expect( + applicationContext.getPersistenceGateway().getCasesForUser, + ).not.toHaveBeenCalled(); + + await sleep(50); + expect( + applicationContext.getPersistenceGateway().getCasesForUser, + ).not.toHaveBeenCalled(); + + resolver!(null); + await sleep(50); + expect( + applicationContext.getPersistenceGateway().getCasesForUser, + ).toHaveBeenCalled(); + }); }); describe('handleLockError', () => { @@ -107,6 +140,10 @@ describe('updateUserContactInformationInteractor', () => { entityName: 'Practitioner', }); + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockResolvedValue(null); + applicationContext .getPersistenceGateway() .getCaseByDocketNumber.mockReturnValue(MOCK_CASE); diff --git a/web-api/src/business/useCases/user/updateUserContactInformationInteractor.ts b/web-api/src/business/useCases/user/updateUserContactInformationInteractor.ts index 0994799fb13..ddb07cd9b44 100644 --- a/web-api/src/business/useCases/user/updateUserContactInformationInteractor.ts +++ b/web-api/src/business/useCases/user/updateUserContactInformationInteractor.ts @@ -112,7 +112,7 @@ const updateUserContactInformationHelper = async ( }); if (isArray(results) && !results.length) { - userEntity.setIsUpdatingInformation(undefined); + userEntity.setIsUpdatingInformation(false); await applicationContext.getPersistenceGateway().updateUser({ applicationContext, user: userEntity.validate().toRawObject(), @@ -198,6 +198,13 @@ export const determineEntitiesToLock = async ( applicationContext: ServerApplicationContext, { userId }: { userId: string }, ) => { + await applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete({ + applicationContext, + userId, + }); + const cases = await applicationContext .getPersistenceGateway() .getCasesForUser({ diff --git a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts index e9e30e6b206..333dcff035f 100644 --- a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts +++ b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts @@ -18,6 +18,7 @@ import { mockPetitionsClerkUser, mockPrivatePractitionerUser, } from '@shared/test/mockAuthUsers'; +import { sleep } from '@shared/tools/helpers'; import { validUser } from '../../../../../shared/src/test/mockUsers'; describe('Verify User Pending Email', () => { @@ -35,6 +36,20 @@ describe('Verify User Pending Email', () => { .minus({ hours: TOKEN_EXPIRATION_TIME_HOURS + 0.001 }) .toISO()!; + beforeEach(() => { + const TOTAL_CASE_COUNT = 100; + + applicationContext + .getPersistenceGateway() + .getDocketNumbersByUser.mockResolvedValue( + Array(TOTAL_CASE_COUNT).fill(undefined), + ); + + applicationContext + .getPersistenceGateway() + .getCasesByEmailTotal.mockReturnValue(TOTAL_CASE_COUNT); + }); + describe('userTokenHasExpired', () => { it('should return true when no token', () => { expect(userTokenHasExpired(undefined)).toBe(true); @@ -99,7 +114,7 @@ describe('Verify User Pending Email', () => { beforeEach(() => { applicationContext .getPersistenceGateway() - .getUserById.mockReturnValue(mockPractitioner); + .getUserByIdOnceAllUpdatesComplete.mockReturnValue(mockPractitioner); applicationContext .getPersistenceGateway() @@ -108,10 +123,6 @@ describe('Verify User Pending Email', () => { applicationContext .getPersistenceGateway() .getCaseByDocketNumber.mockReturnValue(mockCase); - - applicationContext - .getPersistenceGateway() - .getDocketNumbersByUser.mockReturnValue([mockCase.docketNumber]); }); it('should throw unauthorized error when user does not have permission to verify emails', async () => { @@ -139,10 +150,12 @@ describe('Verify User Pending Email', () => { }); it('should throw an unauthorized error when the token passed as an argument and the token store on the user are both undefined', async () => { - applicationContext.getPersistenceGateway().getUserById.mockReturnValue({ - ...mockPractitioner, - pendingEmailVerificationToken: undefined, - }); + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockReturnValue({ + ...mockPractitioner, + pendingEmailVerificationToken: undefined, + }); await expect( verifyUserPendingEmailInteractor( @@ -156,10 +169,12 @@ describe('Verify User Pending Email', () => { }); it('should throw an unauthorized error when there is no token timestamp', async () => { - applicationContext.getPersistenceGateway().getUserById.mockReturnValue({ - ...mockPractitioner, - pendingEmailVerificationTokenTimestamp: undefined, - }); + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockReturnValue({ + ...mockPractitioner, + pendingEmailVerificationTokenTimestamp: undefined, + }); await expect( verifyUserPendingEmailInteractor( @@ -173,10 +188,12 @@ describe('Verify User Pending Email', () => { }); it('should throw an unauthorized error when token timestamp is expired', async () => { - applicationContext.getPersistenceGateway().getUserById.mockReturnValue({ - ...mockPractitioner, - pendingEmailVerificationTokenTimestamp: TOKEN_TIMESTAMP_EXPIRED, - }); + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockReturnValue({ + ...mockPractitioner, + pendingEmailVerificationTokenTimestamp: TOKEN_TIMESTAMP_EXPIRED, + }); await expect( verifyUserPendingEmailInteractor( @@ -269,7 +286,7 @@ describe('Verify User Pending Email', () => { it('should call updateUser with email set to pendingEmail and pending fields set to undefined', async () => { applicationContext .getPersistenceGateway() - .getUserById.mockReturnValue(mockPetitioner); + .getUserByIdOnceAllUpdatesComplete.mockReturnValue(mockPetitioner); await verifyUserPendingEmailInteractor( applicationContext, @@ -290,4 +307,37 @@ describe('Verify User Pending Email', () => { }); }); }); + + describe('verifyUserPendingEmailInteractor - Wait until User is free', () => { + it('should wait until the user record is free to run the interactor', async () => { + let resolver: Function; + let errorMessage: string; + + applicationContext + .getPersistenceGateway() + .getUserByIdOnceAllUpdatesComplete.mockImplementation(() => { + return new Promise(resolve => (resolver = resolve)); + }); + + verifyUserPendingEmailInteractor( + applicationContext, + { + token: 'abc', + }, + mockPrivatePractitionerUser, + ).catch(error => { + errorMessage = error.message; + }); + + await sleep(50); + expect(errorMessage!).toEqual(undefined); + + await sleep(50); + expect(errorMessage!).toEqual(undefined); + + resolver!(mockPrivatePractitionerUser); + await sleep(50); + expect(errorMessage!).toEqual('Tokens do not match'); + }); + }); }); diff --git a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts index 6f9d43e2462..c182cb14482 100644 --- a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts +++ b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts @@ -4,7 +4,7 @@ import { isAuthorized, } from '../../../../../shared/src/authorization/authorizationClientService'; import { ServerApplicationContext } from '@web-api/applicationContext'; -import { UnauthorizedError } from '../../../errors/errors'; +import { UnauthorizedError } from '@web-api/errors/errors'; import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser'; import { calculateDifferenceInHours, @@ -39,7 +39,10 @@ export const verifyUserPendingEmailInteractor = async ( const user = await applicationContext .getPersistenceGateway() - .getUserById({ applicationContext, userId: authorizedUser.userId }); + .getUserByIdOnceAllUpdatesComplete({ + applicationContext, + userId: authorizedUser.userId, + }); if ( !user.pendingEmailVerificationToken || @@ -73,6 +76,7 @@ export const verifyUserPendingEmailInteractor = async ( const { updatedUser } = await updateUserPendingEmailRecord( applicationContext, { + setIsUpdatingInformation: true, user, }, ); @@ -81,14 +85,14 @@ export const verifyUserPendingEmailInteractor = async ( attributesToUpdate: { email: updatedUser.email, }, - email: user.email, + email: user.email!, }); await applicationContext.getWorkerGateway().queueWork(applicationContext, { message: { authorizedUser, payload: { user: updatedUser }, - type: MESSAGE_TYPES.QUEUE_UPDATE_ASSOCIATED_CASES, + type: MESSAGE_TYPES.QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES, }, }); }; diff --git a/web-api/src/database-types.ts b/web-api/src/database-types.ts index c1bee19ec32..f7cbb2296b4 100644 --- a/web-api/src/database-types.ts +++ b/web-api/src/database-types.ts @@ -1,6 +1,7 @@ import { ColumnType, Insertable, Selectable, Updateable } from 'kysely'; export interface Database { + dwUserCaseNote: UserCaseNoteTable; dwMessage: MessageTable; dwCase: CaseTable; } @@ -47,3 +48,13 @@ export interface CaseTable { export type CaseKysely = Selectable; export type NewCaseKysely = Insertable; export type UpdateCaseKysely = Updateable; + +export interface UserCaseNoteTable { + docketNumber: string; + userId: string; + notes?: string; +} + +export type UserCaseNoteKysely = Selectable; +export type NewUserCaseNoteKysely = Insertable; +export type UpdateUserCaseNoteKysely = Updateable; diff --git a/web-api/src/gateways/worker/workerRouter.test.ts b/web-api/src/gateways/worker/workerRouter.test.ts index 29f5b13727a..36b477fcd49 100644 --- a/web-api/src/gateways/worker/workerRouter.test.ts +++ b/web-api/src/gateways/worker/workerRouter.test.ts @@ -51,6 +51,28 @@ describe('workerRouter', () => { ); }); + it('should make a call to queue a user`s email associated cases for update when the message type is QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES', async () => { + const mockMessage: WorkerMessage = { + authorizedUser: mockDocketClerkUser, + payload: { + abc: '123', + }, + type: MESSAGE_TYPES.QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES, + }; + + await workerRouter(applicationContext, { + message: mockMessage, + }); + + expect( + applicationContext.getUseCases().queueEmailUpdateAssociatedCasesWorker, + ).toHaveBeenCalledWith( + applicationContext, + mockMessage.payload, + mockMessage.authorizedUser, + ); + }); + it('should throw an error when the message type provided was not recognized by the router', async () => { const mockMessage: WorkerMessage = { authorizedUser: mockDocketClerkUser, diff --git a/web-api/src/gateways/worker/workerRouter.ts b/web-api/src/gateways/worker/workerRouter.ts index c61b7316710..bad1f9c34d5 100644 --- a/web-api/src/gateways/worker/workerRouter.ts +++ b/web-api/src/gateways/worker/workerRouter.ts @@ -8,6 +8,7 @@ export type WorkerMessage = { }; export const MESSAGE_TYPES = { + QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES: 'QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES', QUEUE_UPDATE_ASSOCIATED_CASES: 'QUEUE_UPDATE_ASSOCIATED_CASES', UPDATE_ASSOCIATED_CASE: 'UPDATE_ASSOCIATED_CASE', } as const; @@ -42,6 +43,15 @@ export const workerRouter = async ( message.authorizedUser, ); break; + case MESSAGE_TYPES.QUEUE_EMAIL_UPDATE_ASSOCIATED_CASES: + await applicationContext + .getUseCases() + .queueEmailUpdateAssociatedCasesWorker( + applicationContext, + message.payload, + message.authorizedUser, + ); + break; default: throw new Error( `No matching router found for message: ${JSON.stringify(message)}`, diff --git a/web-api/src/getPersistenceGateway.ts b/web-api/src/getPersistenceGateway.ts index 3dd1b12b261..bea4f8ff3df 100644 --- a/web-api/src/getPersistenceGateway.ts +++ b/web-api/src/getPersistenceGateway.ts @@ -42,7 +42,6 @@ import { deletePractitionerDocument } from './persistence/dynamo/practitioners/d import { deleteRecord } from './persistence/elasticsearch/deleteRecord'; import { deleteTrialSession } from './persistence/dynamo/trialSessions/deleteTrialSession'; import { deleteTrialSessionWorkingCopy } from './persistence/dynamo/trialSessions/deleteTrialSessionWorkingCopy'; -import { deleteUserCaseNote } from './persistence/dynamo/userCaseNotes/deleteUserCaseNote'; import { deleteUserConnection } from './persistence/dynamo/notifications/deleteUserConnection'; import { deleteUserFromCase } from './persistence/dynamo/cases/deleteUserFromCase'; import { deleteWorkItem } from './persistence/dynamo/workitems/deleteWorkItem'; @@ -65,6 +64,7 @@ import { getCaseMetadataByDocketNumber } from './persistence/dynamo/cases/getCas import { getCaseMetadataWithCounsel } from './persistence/dynamo/cases/getCaseMetadataWithCounsel'; import { getCaseWorksheetsByDocketNumber } from '@web-api/persistence/dynamo/caseWorksheet/getCaseWorksheetsByDocketNumber'; import { getCasesByDocketNumbers } from './persistence/dynamo/cases/getCasesByDocketNumbers'; +import { getCasesByEmailTotal } from '@web-api/persistence/elasticsearch/getCasesByEmailTotal'; import { getCasesByLeadDocketNumber } from './persistence/dynamo/cases/getCasesByLeadDocketNumber'; import { getCasesByUserId } from './persistence/elasticsearch/getCasesByUserId'; import { getCasesClosedCountByJudge } from './persistence/elasticsearch/getCasesClosedCountByJudge'; @@ -116,8 +116,9 @@ import { getTrialSessions } from './persistence/dynamo/trialSessions/getTrialSes import { getUploadPolicy } from './persistence/s3/getUploadPolicy'; import { getUserByEmail } from './persistence/dynamo/users/getUserByEmail'; import { getUserById } from './persistence/dynamo/users/getUserById'; -import { getUserCaseNote } from './persistence/dynamo/userCaseNotes/getUserCaseNote'; -import { getUserCaseNoteForCases } from './persistence/dynamo/userCaseNotes/getUserCaseNoteForCases'; +import { getUserByIdOnceAllUpdatesComplete } from '@web-api/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete'; +import { getUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote'; +import { getUserCaseNoteForCases } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases'; import { getUsersById } from './persistence/dynamo/users/getUsersById'; import { getUsersBySearchKey } from './persistence/dynamo/users/getUsersBySearchKey'; import { getUsersInSection } from './persistence/dynamo/users/getUsersInSection'; @@ -164,7 +165,6 @@ import { updatePractitionerUser } from './persistence/dynamo/users/updatePractit import { updateTrialSession } from './persistence/dynamo/trialSessions/updateTrialSession'; import { updateTrialSessionWorkingCopy } from './persistence/dynamo/trialSessions/updateTrialSessionWorkingCopy'; import { updateUser } from './persistence/dynamo/users/updateUser'; -import { updateUserCaseNote } from './persistence/dynamo/userCaseNotes/updateUserCaseNote'; import { updateUserRecords } from './persistence/dynamo/users/updateUserRecords'; import { uploadDocument } from '@web-api/persistence/s3/uploadDocument'; import { verifyCaseForUser } from './persistence/dynamo/cases/verifyCaseForUser'; @@ -257,7 +257,6 @@ const gatewayMethods = { updateTrialSession, updateTrialSessionWorkingCopy, updateUser, - updateUserCaseNote, updateUserRecords, }), // methods below are not known to create or update "entity" records @@ -277,7 +276,6 @@ const gatewayMethods = { deleteRecord, deleteTrialSession, deleteTrialSessionWorkingCopy, - deleteUserCaseNote, deleteUserConnection, deleteUserFromCase, deleteWorkItem, @@ -298,6 +296,7 @@ const gatewayMethods = { getCaseMetadataWithCounsel, getCaseWorksheetsByDocketNumber, getCasesByDocketNumbers, + getCasesByEmailTotal, getCasesByLeadDocketNumber, getCasesByUserId, getCasesClosedCountByJudge, @@ -349,6 +348,7 @@ const gatewayMethods = { getUploadPolicy, getUserByEmail, getUserById, + getUserByIdOnceAllUpdatesComplete, getUserCaseNote, getUserCaseNoteForCases, getUsersById, diff --git a/web-api/src/getUseCases.ts b/web-api/src/getUseCases.ts index 02b53987ee4..72b03b32784 100644 --- a/web-api/src/getUseCases.ts +++ b/web-api/src/getUseCases.ts @@ -149,6 +149,7 @@ import { orderAdvancedSearchInteractor } from '../../shared/src/business/useCase import { orderPublicSearchInteractor } from './business/useCases/public/orderPublicSearchInteractor'; import { prioritizeCaseInteractor } from '../../shared/src/business/useCases/prioritizeCaseInteractor'; import { processStreamRecordsInteractor } from './business/useCases/processStreamRecords/processStreamRecordsInteractor'; +import { queueEmailUpdateAssociatedCasesWorker } from '@web-api/business/useCases/user/queueEmailUpdateAssociatedCasesWorker'; import { queueUpdateAssociatedCasesWorker } from './business/useCases/user/queueUpdateAssociatedCasesWorker'; import { removeCaseFromTrialInteractor } from './business/useCases/trialSessions/removeCaseFromTrialInteractor'; import { removeCasePendingItemInteractor } from '../../shared/src/business/useCases/removeCasePendingItemInteractor'; @@ -363,6 +364,7 @@ const useCases = { orderPublicSearchInteractor, prioritizeCaseInteractor, processStreamRecordsInteractor, + queueEmailUpdateAssociatedCasesWorker, queueUpdateAssociatedCasesWorker, removeCaseFromTrialInteractor, removeCasePendingItemInteractor, diff --git a/web-api/src/logger.test.ts b/web-api/src/logger.test.ts index ac23105cdbd..9df943eb3e6 100644 --- a/web-api/src/logger.test.ts +++ b/web-api/src/logger.test.ts @@ -61,11 +61,12 @@ describe('logger', () => { const instance = req.locals.logger; instance.info = jest.fn(); + jest.spyOn(instance, 'addContext'); res.end(); - expect(instance.info).toHaveBeenCalledWith( - expect.any(String), + expect(instance.info).toHaveBeenCalledWith(expect.any(String)); + expect(instance.addContext).toHaveBeenCalledWith( expect.objectContaining({ response: expect.objectContaining({ statusCode: 200, diff --git a/web-api/src/logger.ts b/web-api/src/logger.ts index 2bc1e530acd..7843e1a4ab3 100644 --- a/web-api/src/logger.ts +++ b/web-api/src/logger.ts @@ -43,13 +43,15 @@ export const expressLogger = (req, res, next) => { end.apply(this, arguments); const responseTimeMs = new Date() - req.locals.startTime; - logger.info(`Request ended: ${req.method} ${req.url}`, { + logger.addContext({ response: { responseSize: parseInt(res.get('content-length') ?? '0'), responseTimeMs, statusCode: res.statusCode, }, }); + + logger.info(`Request ended: ${req.method} ${req.url}`); logger.clearContext(); }; diff --git a/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.test.ts b/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.test.ts deleted file mode 100644 index 3a285c0efdf..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; -import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { deleteUserCaseNote } from './deleteUserCaseNote'; -import { remove } from '../../dynamodbClientService'; - -jest.mock('../../dynamodbClientService', () => ({ - remove: jest.fn(), -})); - -describe('deleteUserCaseNote', () => { - const USER_ID = '10ecc428-ca35-4e36-aef2-e844660ce22d'; - - it('attempts to delete the case note', async () => { - await deleteUserCaseNote({ - applicationContext, - docketNumber: MOCK_CASE.docketNumber, - userId: USER_ID, - }); - - expect((remove as jest.Mock).mock.calls[0][0]).toMatchObject({ - key: { - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - }, - }); - }); -}); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.ts b/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.ts deleted file mode 100644 index 82c8d0397c4..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/deleteUserCaseNote.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { remove } from '../../dynamodbClientService'; - -/** - * deleteUserCaseNote - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {string} providers.docketNumber the docket number of the case the notes are associated with - * @param {string} providers.userId the id of the user who owns the case notes - * @returns {Array} the promises for the persistence delete calls - */ -export const deleteUserCaseNote = ({ - applicationContext, - docketNumber, - userId, -}: { - applicationContext: IApplicationContext; - docketNumber: string; - userId: string; -}) => - remove({ - applicationContext, - key: { - pk: `user-case-note|${docketNumber}`, - sk: `user|${userId}`, - }, - }); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.test.ts b/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.test.ts deleted file mode 100644 index 566e7849351..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; -import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { get } from '../../dynamodbClientService'; -import { getUserCaseNote } from './getUserCaseNote'; - -const USER_ID = '220b5dc9-9d0c-4662-97ad-2cb9729c611a'; - -jest.mock('../../dynamodbClientService', () => ({ - get: jest.fn().mockReturnValue({ - notes: 'something', - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - userId: USER_ID, - }), -})); - -describe('getUserCaseNote', () => { - it('should get the case notes using docket number and user id', async () => { - const result = await getUserCaseNote({ - applicationContext, - docketNumber: MOCK_CASE.docketNumber, - userId: USER_ID, - }); - - expect((get as jest.Mock).mock.calls[0][0]).toMatchObject({ - Key: { - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - }, - }); - - expect(result).toEqual({ - notes: 'something', - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - userId: USER_ID, - }); - }); -}); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.ts b/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.ts deleted file mode 100644 index b7b9f4d0566..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNote.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { get } from '../../dynamodbClientService'; - -/** - * getUserCaseNote - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {string} providers.docketNumber the docket number of the case to get the case notes for - * @param {string} providers.userId the id of the user to get the case notes for - * @returns {Promise} the promise of the persistence call to get the record - */ -export const getUserCaseNote = ({ - applicationContext, - docketNumber, - userId, -}: { - applicationContext: IApplicationContext; - docketNumber: string; - userId: string; -}) => - get({ - Key: { - pk: `user-case-note|${docketNumber}`, - sk: `user|${userId}`, - }, - applicationContext, - }); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.test.ts b/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.test.ts deleted file mode 100644 index 7d2cdd56c07..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; -import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { getUserCaseNoteForCases } from './getUserCaseNoteForCases'; - -const USER_ID = 'b1edae5a-23e4-4dc8-9d6c-43060ab3d8c7'; - -jest.mock('../../dynamodbClientService', () => ({ - batchGet: jest.fn().mockReturnValue([ - { - notes: 'something', - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - userId: USER_ID, - }, - ]), -})); - -describe('getUserCaseNoteForCases', () => { - it('should get the case notes by case id and user id', async () => { - const result = await getUserCaseNoteForCases({ - applicationContext, - docketNumbers: [MOCK_CASE.docketNumber], - userId: USER_ID, - }); - - expect(result).toEqual([ - { - notes: 'something', - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - userId: USER_ID, - }, - ]); - }); -}); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.ts b/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.ts deleted file mode 100644 index aea2d8f5f9c..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/getUserCaseNoteForCases.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { batchGet } from '../../dynamodbClientService'; - -/** - * getUserCaseNoteForCases - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {Array} providers.docketNumbers the docket numbers of the cases to get the case notes for - * @param {string} providers.userId the id of the user to get the case notes for - * @returns {Promise} the promise of the persistence call to get the record - */ -export const getUserCaseNoteForCases = ({ - applicationContext, - docketNumbers, - userId, -}: { - applicationContext: IApplicationContext; - docketNumbers: string[]; - userId: string; -}) => - batchGet({ - applicationContext, - keys: docketNumbers.map(docketNumber => ({ - pk: `user-case-note|${docketNumber}`, - sk: `user|${userId}`, - })), - }); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.test.ts b/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.test.ts deleted file mode 100644 index 376b3ab973f..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; -import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { put } from '../../dynamodbClientService'; -import { updateUserCaseNote } from './updateUserCaseNote'; - -jest.mock('../../dynamodbClientService', () => ({ - put: jest.fn(), -})); - -describe('updateUserCaseNote', () => { - const USER_ID = '42f68c70-b803-4883-985d-ea1903e31ae2'; - - it('invokes the persistence layer with pk of user-case-note|{docketNumber}, sk of {userId} and other expected params', async () => { - await updateUserCaseNote({ - applicationContext, - caseNoteToUpdate: { - docketNumber: MOCK_CASE.docketNumber, - notes: 'something!!!', - userId: USER_ID, - }, - }); - - expect((put as jest.Mock).mock.calls[0][0]).toMatchObject({ - Item: { - notes: 'something!!!', - pk: `user-case-note|${MOCK_CASE.docketNumber}`, - sk: `user|${USER_ID}`, - }, - }); - }); -}); diff --git a/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.ts b/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.ts deleted file mode 100644 index ba08d161fbf..00000000000 --- a/web-api/src/persistence/dynamo/userCaseNotes/updateUserCaseNote.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { put } from '../../dynamodbClientService'; - -/** - * updateUserCaseNote - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {object} providers.caseNoteToUpdate the case note data to update - * @returns {Promise} the promise of the call to persistence - */ -export const updateUserCaseNote = ({ - applicationContext, - caseNoteToUpdate, -}: { - applicationContext: IApplicationContext; - caseNoteToUpdate: TCaseNote; -}) => - put({ - Item: { - ...caseNoteToUpdate, - pk: `user-case-note|${caseNoteToUpdate.docketNumber}`, - sk: `user|${caseNoteToUpdate.userId}`, - }, - applicationContext, - }); diff --git a/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.test.ts b/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.test.ts new file mode 100644 index 00000000000..4138c6bd746 --- /dev/null +++ b/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.test.ts @@ -0,0 +1,50 @@ +import { UserRecord } from '@web-api/persistence/dynamo/dynamoTypes'; +import { applicationContext } from '@shared/business/test/createTestApplicationContext'; +import { getUserByIdOnceAllUpdatesComplete } from '@web-api/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete'; +import { sleep } from '@shared/tools/helpers'; + +describe('getUserByIdOnceAllUpdatesComplete', () => { + const TEST_USER_ID = 'TEST_USER_ID'; + let RESOLVER: Function; + + beforeEach(() => { + applicationContext + .getPersistenceGateway() + .getUserById.mockImplementation( + () => new Promise(resolve => (RESOLVER = resolve)), + ); + }); + + it('should wait until the user is done updating to return the user record', async () => { + let COMPLETE_FLAG = false; + + void getUserByIdOnceAllUpdatesComplete({ + applicationContext, + userId: TEST_USER_ID, + }).then(userInfo => { + expect(userInfo).toEqual({ isUpdatingInformation: false }); + COMPLETE_FLAG = true; + }); + + let getUserByIdCalls = + applicationContext.getPersistenceGateway().getUserById.mock.calls; + expect(getUserByIdCalls.length).toEqual(1); + RESOLVER({ isUpdatingInformation: true } as UserRecord); + await sleep(50); + expect(COMPLETE_FLAG).toEqual(false); + + getUserByIdCalls = + applicationContext.getPersistenceGateway().getUserById.mock.calls; + expect(getUserByIdCalls.length).toEqual(2); + RESOLVER({ isUpdatingInformation: true } as UserRecord); + await sleep(50); + expect(COMPLETE_FLAG).toEqual(false); + + getUserByIdCalls = + applicationContext.getPersistenceGateway().getUserById.mock.calls; + expect(getUserByIdCalls.length).toEqual(3); + RESOLVER({ isUpdatingInformation: false } as UserRecord); + await sleep(50); + expect(COMPLETE_FLAG).toEqual(true); + }); +}); diff --git a/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.ts b/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.ts new file mode 100644 index 00000000000..f157e19db39 --- /dev/null +++ b/web-api/src/persistence/dynamo/users/getUserByIdOnceAllUpdatesComplete.ts @@ -0,0 +1,21 @@ +import { UserRecord } from '@web-api/persistence/dynamo/dynamoTypes'; + +export const getUserByIdOnceAllUpdatesComplete = async ({ + applicationContext, + userId, +}: { + applicationContext: IApplicationContext; + userId: string; +}): Promise => { + const user = await applicationContext + .getPersistenceGateway() + .getUserById({ applicationContext, userId }); + + if (!user.isUpdatingInformation) return user; + + await applicationContext.getUtilities().sleep(1500); + return await getUserByIdOnceAllUpdatesComplete({ + applicationContext, + userId, + }); +}; diff --git a/web-api/src/persistence/elasticsearch/caseAdvancedSearch.test.ts b/web-api/src/persistence/elasticsearch/caseAdvancedSearch.test.ts index a377a3f4639..a035815ef07 100644 --- a/web-api/src/persistence/elasticsearch/caseAdvancedSearch.test.ts +++ b/web-api/src/persistence/elasticsearch/caseAdvancedSearch.test.ts @@ -5,7 +5,10 @@ import { search } from './searchClient'; describe('caseAdvancedSearch', () => { it('returns results from an exact-matches query', async () => { - search.mockReturnValue({ results: ['some', 'matches'], total: 0 }); + (search as jest.Mock).mockReturnValue({ + results: ['some', 'matches'], + total: 0, + }); const results = await caseAdvancedSearch({ applicationContext, @@ -13,7 +16,9 @@ describe('caseAdvancedSearch', () => { }); expect(search).toHaveBeenCalledTimes(1); - expect(search.mock.calls[0][0].searchParameters.body['_source']).toEqual([ + expect( + (search as jest.Mock).mock.calls[0][0].searchParameters.body['_source'], + ).toEqual([ 'caseCaption', 'petitioners', 'docketNumber', @@ -29,7 +34,7 @@ describe('caseAdvancedSearch', () => { }); it('returns results from an non-exact-matches query when an exact query returns no results', async () => { - search + (search as jest.Mock) .mockImplementation(() => { // default behavior return Promise.resolve({ results: ['other', 'matches'], total: 2 }); diff --git a/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.test.ts b/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.test.ts new file mode 100644 index 00000000000..564a07dbf28 --- /dev/null +++ b/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.test.ts @@ -0,0 +1,59 @@ +import { applicationContext } from '@shared/business/test/createTestApplicationContext'; +import { getCasesByEmailTotal } from '@web-api/persistence/elasticsearch/getCasesByEmailTotal'; +jest.mock('./searchClient'); +import { search } from './searchClient'; + +describe('getCasesByEmailTotal', () => { + const TEST_EMAIL = 'TEST_EMAIL'; + const RESULTS_MOCK = 99999; + + beforeEach(() => { + (search as jest.Mock).mockImplementation(() => ({ total: RESULTS_MOCK })); + }); + + it('should call search with correct parameters', async () => { + const results = await getCasesByEmailTotal({ + applicationContext, + email: TEST_EMAIL, + }); + + const searchCalls = (search as jest.Mock).mock.calls; + expect(searchCalls.length).toEqual(1); + expect(searchCalls[0][0].searchParameters).toEqual({ + body: { + query: { + bool: { + minimum_should_match: 1, + must: [ + { + term: { + 'entityName.S': 'Case', + }, + }, + ], + should: [ + { + term: { + 'privatePractitioners.L.M.email.S': TEST_EMAIL, + }, + }, + { + term: { + 'irsPractitioners.L.M.email.S': TEST_EMAIL, + }, + }, + { + term: { + 'petitioners.L.M.email.S': TEST_EMAIL, + }, + }, + ], + }, + }, + }, + index: 'efcms-case', + }); + + expect(results).toEqual(RESULTS_MOCK); + }); +}); diff --git a/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.ts b/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.ts new file mode 100644 index 00000000000..bd41e6b014f --- /dev/null +++ b/web-api/src/persistence/elasticsearch/getCasesByEmailTotal.ts @@ -0,0 +1,53 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; +import { search } from './searchClient'; + +type GetCasesByEmailParams = { + applicationContext: ServerApplicationContext; + email: string; +}; +export const getCasesByEmailTotal = async ({ + applicationContext, + email, +}: GetCasesByEmailParams) => { + const searchParameters = { + body: { + query: { + bool: { + minimum_should_match: 1, + must: [ + { + term: { + 'entityName.S': 'Case', + }, + }, + ], + should: [ + { + term: { + 'privatePractitioners.L.M.email.S': email, + }, + }, + { + term: { + 'irsPractitioners.L.M.email.S': email, + }, + }, + { + term: { + 'petitioners.L.M.email.S': email, + }, + }, + ], + }, + }, + }, + index: 'efcms-case', + }; + + const result = await search({ + applicationContext, + searchParameters, + }); + + return result.total; +}; diff --git a/web-api/src/persistence/postgres/userCaseNotes/deleteUserCaseNote.ts b/web-api/src/persistence/postgres/userCaseNotes/deleteUserCaseNote.ts new file mode 100644 index 00000000000..56af9163386 --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/deleteUserCaseNote.ts @@ -0,0 +1,17 @@ +import { getDbWriter } from '@web-api/database'; + +export const deleteUserCaseNote = async ({ + docketNumber, + userId, +}: { + docketNumber: string; + userId: string; +}) => { + await getDbWriter(writer => + writer + .deleteFrom('dwUserCaseNote') + .where('docketNumber', '=', docketNumber) + .where('userId', '=', userId) + .execute(), + ); +}; diff --git a/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNote.ts b/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNote.ts new file mode 100644 index 00000000000..c6db8c43076 --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNote.ts @@ -0,0 +1,22 @@ +import { UserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; +import { getDbReader } from '@web-api/database'; +import { transformNullToUndefined } from '@web-api/persistence/postgres/utils/transformNullToUndefined'; + +export const getUserCaseNote = async ({ + docketNumber, + userId, +}: { + docketNumber: string; + userId: string; +}) => { + const userCaseNote = await getDbReader(writer => + writer + .selectFrom('dwUserCaseNote') + .selectAll() + .where('docketNumber', '=', docketNumber) + .where('userId', '=', userId) + .executeTakeFirst(), + ); + + return new UserCaseNote(transformNullToUndefined(userCaseNote)); +}; diff --git a/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNoteForCases.ts b/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNoteForCases.ts new file mode 100644 index 00000000000..6e560147180 --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/getUserCaseNoteForCases.ts @@ -0,0 +1,24 @@ +import { UserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; +import { getDbReader } from '@web-api/database'; +import { transformNullToUndefined } from '@web-api/persistence/postgres/utils/transformNullToUndefined'; + +export const getUserCaseNoteForCases = async ({ + docketNumbers, + userId, +}: { + docketNumbers: string[]; + userId: string; +}) => { + const userCaseNotes = await getDbReader(reader => + reader + .selectFrom('dwUserCaseNote') + .selectAll() + .where('userId', '=', userId) + .where('docketNumber', 'in', docketNumbers) + .execute(), + ); + + return userCaseNotes.map( + userCaseNote => new UserCaseNote(transformNullToUndefined(userCaseNote)), + ); +}; diff --git a/web-api/src/persistence/postgres/userCaseNotes/mocks.jest.ts b/web-api/src/persistence/postgres/userCaseNotes/mocks.jest.ts new file mode 100644 index 00000000000..e13040c7cd9 --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/mocks.jest.ts @@ -0,0 +1,26 @@ +import { mockFactory } from '@shared/test/mockFactory'; + +jest.mock( + '@web-api/persistence/postgres/userCaseNotes/deleteUserCaseNote.ts', + () => mockFactory('deleteUserCaseNote'), +); + +jest.mock( + '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote.ts', + () => mockFactory('getUserCaseNote'), +); + +jest.mock( + '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases.ts', + () => mockFactory('getUserCaseNoteForCases'), +); + +jest.mock( + '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNote.ts', + () => mockFactory('upsertUserCaseNote'), +); + +jest.mock( + '@web-api/persistence/postgres/userCaseNotes/upsertUserCaseNotes.ts', + () => mockFactory('upsertUserCaseNotes'), +); diff --git a/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNote.ts b/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNote.ts new file mode 100644 index 00000000000..a038d5615fb --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNote.ts @@ -0,0 +1,24 @@ +import { UserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; +import { getDbWriter } from '@web-api/database'; + +export const upsertUserCaseNote = async ({ + caseNoteToUpsert, +}: { + caseNoteToUpsert: UserCaseNote; +}) => { + await getDbWriter(writer => + writer + .insertInto('dwUserCaseNote') + .values({ + docketNumber: caseNoteToUpsert.docketNumber, + notes: caseNoteToUpsert.notes, + userId: caseNoteToUpsert.userId, + }) + .onConflict(oc => + oc.columns(['docketNumber', 'userId']).doUpdateSet({ + notes: caseNoteToUpsert.notes, + }), + ) + .execute(), + ); +}; diff --git a/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNotes.ts b/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNotes.ts new file mode 100644 index 00000000000..dd951929521 --- /dev/null +++ b/web-api/src/persistence/postgres/userCaseNotes/upsertUserCaseNotes.ts @@ -0,0 +1,26 @@ +import { RawUserCaseNote } from '@shared/business/entities/notes/UserCaseNote'; +import { getDbWriter } from '@web-api/database'; + +export const upsertUserCaseNotes = async (userCaseNotes: RawUserCaseNote[]) => { + if (userCaseNotes.length === 0) return; + + const userCaseNotesToUpsert = userCaseNotes.map(rawUserCaseNote => ({ + docketNumber: rawUserCaseNote.docketNumber, + notes: rawUserCaseNote.notes, + userId: rawUserCaseNote.userId, + })); + + await getDbWriter(writer => + writer + .insertInto('dwUserCaseNote') + .values(userCaseNotesToUpsert) + .onConflict(oc => + oc.columns(['docketNumber', 'userId']).doUpdateSet(c => { + return { + notes: c.ref('excluded.notes'), + }; + }), + ) + .execute(), + ); +}; diff --git a/web-api/src/persistence/postgres/utils/migrate/migrations/0003-add-user-case-notes.ts b/web-api/src/persistence/postgres/utils/migrate/migrations/0003-add-user-case-notes.ts new file mode 100644 index 00000000000..6d21457ecbf --- /dev/null +++ b/web-api/src/persistence/postgres/utils/migrate/migrations/0003-add-user-case-notes.ts @@ -0,0 +1,15 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('dwUserCaseNote') + .addColumn('docketNumber', 'varchar', col => col.notNull()) + .addColumn('userId', 'varchar', col => col.notNull()) + .addColumn('notes', 'text') + .addPrimaryKeyConstraint('pk_user_case_note', ['docketNumber', 'userId']) + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('dwUserCaseNote').execute(); +} diff --git a/web-api/storage/fixtures/seed/efcms-local.json b/web-api/storage/fixtures/seed/efcms-local.json index 44d0e2472b8..70fe522d409 100644 --- a/web-api/storage/fixtures/seed/efcms-local.json +++ b/web-api/storage/fixtures/seed/efcms-local.json @@ -58524,5 +58524,13 @@ "pk": "user|f0a1e52a-876f-4c03-853c-f66e407e5a1e", "userId": "f0a1e52a-876f-4c03-853c-f66e407e5a1e", "email": "trialclerk@example.com" + }, + { + "pk": "user-case-note|107-19", + "sk": "user|dabbad00-18d0-43ec-bafb-654e83405416", + "docketNumber": "107-19", + "entityName": "UserCaseNote", + "notes": "Test", + "userId": "dabbad00-18d0-43ec-bafb-654e83405416" } ] diff --git a/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.test.ts b/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.test.ts index 6d89a183086..a5ac411f5e8 100644 --- a/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.test.ts +++ b/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.test.ts @@ -1,5 +1,6 @@ import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; import { applicationContextForClient as applicationContext } from '@web-client/test/createClientTestApplicationContext'; +import { judgeColvin } from '@shared/test/mockUsers'; import { presenter } from '../../presenter-mock'; import { runAction } from '@web-client/presenter/test.cerebral'; import { setCourtIssuedDocumentInitialDataAction } from './setCourtIssuedDocumentInitialDataAction'; @@ -8,6 +9,8 @@ describe('setCourtIssuedDocumentInitialDataAction', () => { const docketEntryIds = [ 'ddfd978d-6be6-4877-b004-2b5735a41fee', '11597d22-0874-4c5e-ac98-a843d1472baf', + '22597d22-0874-4c5e-ac98-a843d1472baf', + '43737877-0874-4c5e-ac98-dhd83838887j', ]; beforeAll(() => { @@ -22,6 +25,18 @@ describe('setCourtIssuedDocumentInitialDataAction', () => { eventCode: 'O', freeText: 'something', }); + MOCK_CASE.docketEntries.push({ + docketEntryId: docketEntryIds[2], + eventCode: 'OJR', + signedByUserId: judgeColvin.userId, + signedJudgeName: judgeColvin.judgeFullName, + }); + MOCK_CASE.docketEntries.push({ + docketEntryId: docketEntryIds[3], + eventCode: 'OJR', + signedByUserId: 'not-colvins-id', + signedJudgeName: judgeColvin.judgeFullName, + }); }); it('should set correct values on state.form for the docketEntryId passed in via props', async () => { @@ -108,4 +123,50 @@ describe('setCourtIssuedDocumentInitialDataAction', () => { expect(result.state.form).toEqual({}); }); + + it('should set the judge name when eventcode is OJR and docketEntry was signed by the judge', async () => { + const result = await runAction(setCourtIssuedDocumentInitialDataAction, { + modules: { + presenter, + }, + props: { + docketEntryId: docketEntryIds[2], + }, + state: { + caseDetail: MOCK_CASE, + form: {}, + judges: [ + { + judgeFullName: judgeColvin.judgeFullName, + name: judgeColvin.name, + userId: judgeColvin.userId, + }, + ], + }, + }); + expect(result.state.form.judge).toEqual('Colvin'); + }); + + it('should set the judge name when eventcode is OJR and docketEntry was signed by a non judge user', async () => { + const result = await runAction(setCourtIssuedDocumentInitialDataAction, { + modules: { + presenter, + }, + props: { + docketEntryId: docketEntryIds[3], + }, + state: { + caseDetail: MOCK_CASE, + form: {}, + judges: [ + { + judgeFullName: judgeColvin.judgeFullName, + name: judgeColvin.name, + userId: judgeColvin.userId, + }, + ], + }, + }); + expect(result.state.form.judge).toEqual('Colvin'); + }); }); diff --git a/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.ts b/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.ts index a99dd05072c..92e9db0309a 100644 --- a/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.ts +++ b/web-client/src/presenter/actions/CourtIssuedDocketEntry/setCourtIssuedDocumentInitialDataAction.ts @@ -18,6 +18,8 @@ export const setCourtIssuedDocumentInitialDataAction = ({ }: ActionProps) => { const { docketEntries } = get(state.caseDetail); + const judges = get(state.judges); + const docketEntry = docketEntries.find( item => item.docketEntryId === props.docketEntryId, ); @@ -34,6 +36,20 @@ export const setCourtIssuedDocumentInitialDataAction = ({ store.set(state.form.attachments, false); } + if (docketEntry.eventCode === 'OJR') { + const signingJudge = judges.find(judge => { + return ( + judge.judgeFullName && + docketEntry.signedJudgeName && + judge.judgeFullName === docketEntry.signedJudgeName + ); + }); + + if (signingJudge) { + store.set(state.form.judge, signingJudge.name); + } + } + if (docketEntry.freeText) { store.set(state.form.freeText, docketEntry.freeText); } diff --git a/web-client/src/presenter/actions/setAlertInfoAction.test.ts b/web-client/src/presenter/actions/setAlertInfoAction.test.ts new file mode 100644 index 00000000000..0e5403d0f3e --- /dev/null +++ b/web-client/src/presenter/actions/setAlertInfoAction.test.ts @@ -0,0 +1,15 @@ +import { runAction } from '@web-client/presenter/test.cerebral'; +import { setAlertInfoAction } from '@web-client/presenter/actions/setAlertInfoAction'; + +describe('setAlertInfoAction', () => { + it('should set alertInfo in state correctly', async () => { + const TEST_ALERT_INFO = 'TEST_ALERT_INFO'; + const { state } = await runAction(setAlertInfoAction, { + props: { + alertInfo: TEST_ALERT_INFO, + }, + }); + + expect(state.alertInfo).toEqual(TEST_ALERT_INFO); + }); +}); diff --git a/web-client/src/presenter/actions/setAlertInfoAction.ts b/web-client/src/presenter/actions/setAlertInfoAction.ts new file mode 100644 index 00000000000..cf6e7885db5 --- /dev/null +++ b/web-client/src/presenter/actions/setAlertInfoAction.ts @@ -0,0 +1,8 @@ +import { state } from '@web-client/presenter/app.cerebral'; + +export const setAlertInfoAction = ({ + props, + store, +}: ActionProps<{ alertInfo: any }>) => { + store.set(state.alertInfo, props.alertInfo); +}; diff --git a/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.test.ts b/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.test.ts new file mode 100644 index 00000000000..538a687c79e --- /dev/null +++ b/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.test.ts @@ -0,0 +1,15 @@ +import { runAction } from '@web-client/presenter/test.cerebral'; +import { setInitialVerifyAlertMessageAction } from '@web-client/presenter/actions/setInitialVerifyAlertMessageAction'; + +describe('setInitialVerifyAlertMessageAction', () => { + it('should return the correct alertInfo data', async () => { + const { output } = await runAction(setInitialVerifyAlertMessageAction, {}); + + expect(output).toEqual({ + alertInfo: { + message: 'DAWSON is updating your email. Please wait.', + title: 'Updating email address', + }, + }); + }); +}); diff --git a/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.ts b/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.ts new file mode 100644 index 00000000000..75a5658d7b6 --- /dev/null +++ b/web-client/src/presenter/actions/setInitialVerifyAlertMessageAction.ts @@ -0,0 +1,8 @@ +export const setInitialVerifyAlertMessageAction = () => { + return { + alertInfo: { + message: 'DAWSON is updating your email. Please wait.', + title: 'Updating email address', + }, + }; +}; diff --git a/web-client/src/presenter/actions/verifyUserPendingEmailAction.test.tsx b/web-client/src/presenter/actions/verifyUserPendingEmailAction.test.tsx index a56ff71a1de..d91e5fc7853 100644 --- a/web-client/src/presenter/actions/verifyUserPendingEmailAction.test.tsx +++ b/web-client/src/presenter/actions/verifyUserPendingEmailAction.test.tsx @@ -1,3 +1,4 @@ +import { GatewayTimeoutError } from '@web-client/presenter/errors/GatewayTimeoutError'; import { TROUBLESHOOTING_INFO } from '@shared/business/entities/EntityConstants'; import { applicationContextForClient as applicationContext } from '@web-client/test/createClientTestApplicationContext'; import { presenter } from '../presenter-mock'; @@ -20,15 +21,20 @@ describe('verifyUserPendingEmailAction', () => { }); it('should return a success message when the user`s pending email is successfully verified', async () => { - await runAction(verifyUserPendingEmailAction, { + const { state } = await runAction(verifyUserPendingEmailAction, { modules: { presenter, }, props: { token: mockToken, }, + state: { + alertInfo: 'TEST_ALERT_INFO', + }, }); + expect(state.alertInfo).toBeUndefined(); + expect( applicationContext.getUseCases().verifyUserPendingEmailInteractor, ).toHaveBeenCalledWith(expect.anything(), { token: mockToken }); @@ -100,4 +106,33 @@ describe('verifyUserPendingEmailAction', () => { }, }); }); + + it('should return an error message when the request timed out', async () => { + applicationContext + .getUseCases() + .verifyUserPendingEmailInteractor.mockRejectedValue( + new GatewayTimeoutError(), + ); + + await runAction(verifyUserPendingEmailAction, { + modules: { + presenter, + }, + props: { + token: mockToken, + }, + }); + + expect(errorMock).toHaveBeenCalledWith({ + alertError: { + message: ( + <> + DAWSON is updating your other contact information. Please wait and + try to verify your email in a few minutes. + + ), + title: 'DAWSON can’t verify your email right now.', + }, + }); + }); }); diff --git a/web-client/src/presenter/actions/verifyUserPendingEmailAction.tsx b/web-client/src/presenter/actions/verifyUserPendingEmailAction.tsx index 666de86a738..9555a2398dd 100644 --- a/web-client/src/presenter/actions/verifyUserPendingEmailAction.tsx +++ b/web-client/src/presenter/actions/verifyUserPendingEmailAction.tsx @@ -1,4 +1,6 @@ +import { GatewayTimeoutErrorTitle } from '@web-client/presenter/errors/GatewayTimeoutError'; import { TROUBLESHOOTING_INFO } from '@shared/business/entities/EntityConstants'; +import { state } from '@web-client/presenter/app.cerebral'; import React from 'react'; const expiredTokenAlertError = { @@ -11,7 +13,17 @@ const expiredTokenAlertError = { title: 'Verification email link expired', }; -const genericAlertError = { +const requestTimedOutAlertError = { + message: ( + <> + DAWSON is updating your other contact information. Please wait and try to + verify your email in a few minutes. + + ), + title: 'DAWSON can’t verify your email right now.', +}; + +export const genericAlertError = { message: ( <> Your request cannot be completed. Please try to log in. If you’re still @@ -29,6 +41,7 @@ export const verifyUserPendingEmailAction = async ({ applicationContext, path, props, + store, }: ActionProps<{ token: string }>) => { const { token } = props; @@ -38,6 +51,7 @@ export const verifyUserPendingEmailAction = async ({ .verifyUserPendingEmailInteractor(applicationContext, { token, }); + store.unset(state.alertInfo); return path.success({ alertSuccess: { @@ -47,11 +61,18 @@ export const verifyUserPendingEmailAction = async ({ }, }); } catch (e: any) { + store.unset(state.alertInfo); if (e.message === 'Link has expired') { return path.error({ alertError: expiredTokenAlertError, }); } + if (e.title === GatewayTimeoutErrorTitle) { + return path.error({ + alertError: requestTimedOutAlertError, + }); + } + return path.error({ alertError: genericAlertError, }); diff --git a/web-client/src/presenter/errors/GatewayTimeoutError.ts b/web-client/src/presenter/errors/GatewayTimeoutError.ts index 81a44022747..d67e9c35f60 100644 --- a/web-client/src/presenter/errors/GatewayTimeoutError.ts +++ b/web-client/src/presenter/errors/GatewayTimeoutError.ts @@ -1,11 +1,13 @@ import { ActionError } from './ActionError'; +export const GatewayTimeoutErrorTitle = + 'The system is taking too long to respond'; export class GatewayTimeoutError extends ActionError { // HTTP 504 constructor() { const message = 'Try again.'; super(message); - this.title = 'The system is taking too long to respond'; + this.title = GatewayTimeoutErrorTitle; this.message = message; } } diff --git a/web-client/src/presenter/sequences/gotoAddCourtIssuedDocketEntrySequence.ts b/web-client/src/presenter/sequences/gotoAddCourtIssuedDocketEntrySequence.ts index d115deb4354..20c959566fb 100644 --- a/web-client/src/presenter/sequences/gotoAddCourtIssuedDocketEntrySequence.ts +++ b/web-client/src/presenter/sequences/gotoAddCourtIssuedDocketEntrySequence.ts @@ -1,4 +1,5 @@ import { clearFormAction } from '../actions/clearFormAction'; +import { computeJudgeNameWithTitleAction } from '@web-client/presenter/actions/computeJudgeNameWithTitleAction'; import { generateCourtIssuedDocumentTitleAction } from '../actions/CourtIssuedDocketEntry/generateCourtIssuedDocumentTitleAction'; import { getCaseAction } from '../actions/getCaseAction'; import { getFilterCurrentJudgeUsersAction } from '../actions/getFilterCurrentJudgeUsersAction'; @@ -28,6 +29,7 @@ export const gotoAddCourtIssuedDocketEntrySequence = setDocketEntryIdAction, setCourtIssuedDocumentInitialDataAction, setDefaultServiceStampAction, + computeJudgeNameWithTitleAction, generateCourtIssuedDocumentTitleAction, setIsEditingDocketEntryAction(false), setupCurrentPageAction('CourtIssuedDocketEntry'), diff --git a/web-client/src/presenter/sequences/gotoVerifyEmailSequence.ts b/web-client/src/presenter/sequences/gotoVerifyEmailSequence.ts index 65ad6bd2409..4212094a083 100644 --- a/web-client/src/presenter/sequences/gotoVerifyEmailSequence.ts +++ b/web-client/src/presenter/sequences/gotoVerifyEmailSequence.ts @@ -1,10 +1,16 @@ import { clearUserAction } from '../actions/clearUserAction'; +import { gotoLoginSequence } from '@web-client/presenter/sequences/Login/gotoLoginSequence'; import { navigateToLoginAction } from '@web-client/presenter/actions/Login/navigateToLoginAction'; import { setAlertErrorAction } from '@web-client/presenter/actions/setAlertErrorAction'; +import { setAlertInfoAction } from '@web-client/presenter/actions/setAlertInfoAction'; import { setAlertSuccessAction } from '@web-client/presenter/actions/setAlertSuccessAction'; +import { setInitialVerifyAlertMessageAction } from '@web-client/presenter/actions/setInitialVerifyAlertMessageAction'; import { verifyUserPendingEmailAction } from '../actions/verifyUserPendingEmailAction'; export const gotoVerifyEmailSequence = [ + setInitialVerifyAlertMessageAction, + setAlertInfoAction, + gotoLoginSequence, verifyUserPendingEmailAction, { error: [setAlertErrorAction], diff --git a/web-client/src/views/CourtIssuedDocketEntry/CourtIssuedDocketEntry.tsx b/web-client/src/views/CourtIssuedDocketEntry/CourtIssuedDocketEntry.tsx index a0130b97787..8a0bea98655 100644 --- a/web-client/src/views/CourtIssuedDocketEntry/CourtIssuedDocketEntry.tsx +++ b/web-client/src/views/CourtIssuedDocketEntry/CourtIssuedDocketEntry.tsx @@ -96,7 +96,10 @@ export const CourtIssuedDocketEntry = connect(
-
+
Docket entry preview: {addCourtIssuedDocketEntryHelper.formattedDocumentTitle}
diff --git a/web-client/src/views/DocketRecord/DocumentViewer.tsx b/web-client/src/views/DocketRecord/DocumentViewer.tsx index fa3e8970303..811ae7e6d95 100644 --- a/web-client/src/views/DocketRecord/DocumentViewer.tsx +++ b/web-client/src/views/DocketRecord/DocumentViewer.tsx @@ -106,12 +106,21 @@ export const DocumentViewer = connect(
{entry.descriptionDisplay} - {entry.isStricken && ' (STRICKEN)'} + + {entry.isStricken && ' (STRICKEN)'} +
{entry.showNotServed && ( diff --git a/web-client/src/views/Login/Login.tsx b/web-client/src/views/Login/Login.tsx index 6a45d00d17f..3f554f88d6b 100644 --- a/web-client/src/views/Login/Login.tsx +++ b/web-client/src/views/Login/Login.tsx @@ -1,5 +1,6 @@ import { Button } from '@web-client/ustc-ui/Button/Button'; import { ErrorNotification } from '@web-client/views/ErrorNotification'; +import { InfoNotificationComponent } from '@web-client/views/InfoNotification'; import { SuccessNotification } from '@web-client/views/SuccessNotification'; import { WarningNotification } from '@web-client/views/WarningNotification'; import { connect } from '@web-client/presenter/shared.cerebral'; @@ -8,6 +9,7 @@ import React from 'react'; export const Login = connect( { + alertInfo: state.alertInfo, navigateToCreatePetitionerAccountSequence: sequences.navigateToCreatePetitionerAccountSequence, navigateToForgotPasswordSequence: @@ -19,6 +21,7 @@ export const Login = connect( sequences.updateAuthenticationFormValueSequence, }, ({ + alertInfo, navigateToCreatePetitionerAccountSequence, navigateToForgotPasswordSequence, showPassword, @@ -33,6 +36,12 @@ export const Login = connect(
+ {alertInfo && ( + + )}
diff --git a/web-client/src/views/StatusReportOrder.tsx b/web-client/src/views/StatusReportOrder.tsx index 09247c74aee..b1fc668d836 100644 --- a/web-client/src/views/StatusReportOrder.tsx +++ b/web-client/src/views/StatusReportOrder.tsx @@ -155,6 +155,7 @@ export const StatusReportOrder = connect( .statusReport } className="usa-radio__input" + data-testid="order-type-status-report" id="order-type-status-report" name="orderType" type="radio"