From b02ef4f17804da4f6261eb04995f8766ce349fc5 Mon Sep 17 00:00:00 2001 From: ruizajtruss <111928238+ruizajtruss@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:09:28 -0700 Subject: [PATCH] Move actuary contacts section to rate details page (#2365) * WIP: moved actuary radio group * form validation * removal from contacts page * added flexibility to span * added addtlActuaryContacts * dynamic addtnlActuaryContacts form working wip * testing * removed old tests * test fixes * removing unneeded work after pivoting back to dynamic functionality * removed cypress tests * cypress fixes * more test fixes * v1 ratedetails * moved comms preference, still need to follow up with fixing tests * v2 test fixes * v1 test fixes * test fix * test fixes * something isnt working here WIP * cypress fixes * updating test * cypress fixes * Update protobuffer functions for rate additl actuary workaround. Update zod. * Only save addtl actuaries at the rate level. * Update API and helpers with rate level addtl actuaries workaround. * Use addtlActuaryContacts from rate data. * Display addtl actuaries from rate level. * Update for addtl actuary change. * test fix * small fix for cypress * A comment * Another comment --------- Co-authored-by: Jason Lin Co-authored-by: MacRae Linton --- .../convertContractWithRatesToHPP.ts | 21 +- .../contract/updateDraftContractRates.ts | 13 +- .../contractAndRates/resolverHelper.test.ts | 30 +- .../contractAndRates/resolverHelpers.ts | 5 +- .../unlockHealthPlanPackage.test.ts | 12 +- .../updateHealthPlanFormData.test.ts | 45 ++- .../healthPlanFormData.ts | 125 +++----- .../UnlockedHealthPlanFormDataType.ts | 1 + .../proto/healthPlanFormDataProto/toDomain.ts | 13 +- .../healthPlanFormDataProto/toProtoBuffer.ts | 39 +-- .../healthPlanFormDataProto/zodSchemas.ts | 1 + .../Modal/V2/UnlockSubmitModalV2.tsx | 10 +- .../ContactsSummarySection.test.tsx | 30 +- .../ContactsSummarySection.tsx | 14 +- .../RateDetailsSummarySection.tsx | 20 +- .../SingleRateSummarySection.tsx | 2 +- .../UploadedDocumentsTable.test.tsx | 36 ++- .../UploadedDocumentsTable.tsx | 42 ++- .../app-web/src/formHelpers/formatters.ts | 16 + .../Contacts/ActuaryContactFields.tsx | 14 +- .../Contacts/Contacts.test.tsx | 287 ----------------- .../StateSubmission/Contacts/Contacts.tsx | 296 +----------------- .../RateDetails/RateDetails.test.tsx | 10 +- .../RateDetails/RateDetails.tsx | 147 ++++++++- .../RateDetails/RateDetailsSchema.test.ts | 3 +- .../RateDetails/RateDetailsSchema.ts | 32 +- .../RateDetails/SingleRateCert.tsx | 94 +++++- .../RateDetails/V2/RateDetailsV2.test.tsx | 81 ++++- .../RateDetails/V2/RateDetailsV2.tsx | 129 +++++++- .../RateDetails/V2/SingleRateFormFields.tsx | 88 +++++- .../RateDetails/V2/rateDetailsHelpers.ts | 5 +- .../ContractDetailsSummarySectionV2.tsx | 2 +- .../V2/ReviewSubmit/ReviewSubmitV2.tsx | 5 +- .../StateSubmission/StateSubmissionForm.tsx | 5 +- .../SubmissionSummary.test.tsx | 8 +- .../apolloMocks/healthPlanFormDataMock.ts | 17 +- .../src/testHelpers/jestRateHelpers.tsx | 18 +- .../cmsWorkflow/unlockResubmit.spec.ts | 24 +- .../cmsWorkflow/viewSubmission.spec.ts | 10 +- .../stateWorkflow/dashboard/dashboard.spec.ts | 10 +- .../stateSubmissionForm/contacts.spec.ts | 49 +-- .../stateSubmissionForm/rateDetails.spec.ts | 26 +- .../support/stateSubmissionFormCommands.ts | 84 +++-- 43 files changed, 1000 insertions(+), 919 deletions(-) diff --git a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts index 71f4b077f8..82e7eb14de 100644 --- a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts +++ b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts @@ -1,6 +1,5 @@ import type { ActuaryCommunicationType, - ActuaryContact, HealthPlanFormDataType, RateInfoType, SubmissionDocument, @@ -17,7 +16,6 @@ import { import type { ContractRevisionWithRatesType } from './revisionTypes' import { parsePartialHPFD } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto/toDomain' import type { PartialHealthPlanFormData } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto/toDomain' -import { isEqualData } from '../../resolvers/healthPlanPackage/contractAndRates/resolverHelpers' function convertContractWithRatesToUnlockedHPP( contract: ContractType @@ -91,7 +89,6 @@ function convertContractWithRatesToFormData( stateNumber: number ): HealthPlanFormDataType | Error { // additional certifying actuaries are on every rate post refactor but on the package pre-refactor - const pkgAdditionalCertifyingActuaries: ActuaryContact[] = [] let pkgActuaryCommsPref: ActuaryCommunicationType | undefined = undefined const rateInfos: RateInfoType[] = contractRev.rateRevisions.map( @@ -114,16 +111,6 @@ function convertContractWithRatesToFormData( actuaryCommunicationPreference, } = rateRev.formData - for (const additionalActuary of addtlActuaryContacts) { - if ( - !pkgAdditionalCertifyingActuaries.find((actuary) => - isEqualData(actuary, additionalActuary) - ) - ) { - pkgAdditionalCertifyingActuaries.push(additionalActuary) - } - } - // The first time we find a rate that has an actuary comms pref, we use that to set the package's prefs if (actuaryCommunicationPreference && !pkgActuaryCommsPref) { pkgActuaryCommsPref = actuaryCommunicationPreference @@ -151,6 +138,7 @@ function convertContractWithRatesToFormData( rateProgramIDs, rateCertificationName, actuaryContacts: certifyingActuaryContacts ?? [], + addtlActuaryContacts: addtlActuaryContacts ?? [], actuaryCommunicationPreference, packagesWithSharedRateCerts, } @@ -171,7 +159,6 @@ function convertContractWithRatesToFormData( submissionDescription: contractRev.formData.submissionDescription, stateContacts: contractRev.formData.stateContacts, addtlActuaryCommunicationPreference: pkgActuaryCommsPref, - addtlActuaryContacts: [...pkgAdditionalCertifyingActuaries], documents: contractRev.formData.supportingDocuments.map((doc) => ({ ...doc, })) as SubmissionDocument[], @@ -225,6 +212,12 @@ function convertContractWithRatesToFormData( contractRev.formData.modifiedNonRiskPaymentArrangements, }, }, + /** + * This field is unused and will need cleaned up, additional actuaries have been moved to the rate level. + * Leaving this as am empty array to get linked rates feature work in without having to fix all the broken tests + * by removing this field. + */ + addtlActuaryContacts: [], statutoryRegulatoryAttestation: contractRev.formData.statutoryRegulatoryAttestation, statutoryRegulatoryAttestationDescription: diff --git a/services/app-api/src/resolvers/contract/updateDraftContractRates.ts b/services/app-api/src/resolvers/contract/updateDraftContractRates.ts index 064e6855c8..b3a5e281c8 100644 --- a/services/app-api/src/resolvers/contract/updateDraftContractRates.ts +++ b/services/app-api/src/resolvers/contract/updateDraftContractRates.ts @@ -162,13 +162,12 @@ function updateDraftContractRates( let thisPosition = 1 for (const rateUpdate of parsedUpdates) { if (rateUpdate.type === 'CREATE') { - // set rateName for now https://jiraent.cms.gov/browse/MCR-4012 const rateName = generateRateCertificationName( rateUpdate.formData, contract.stateCode, contract.stateNumber, - statePrograms, + statePrograms ) rateUpdates.create.push({ @@ -206,7 +205,12 @@ function updateDraftContractRates( throw new Error(errmsg) } - if (!(rateToUpdate.status === 'DRAFT' || rateToUpdate.status === 'UNLOCKED')) { + if ( + !( + rateToUpdate.status === 'DRAFT' || + rateToUpdate.status === 'UNLOCKED' + ) + ) { // eventually, this will be enough to cancel this. But until we have unlock-rate, you can edit UNLOCKED children of this contract. const errmsg = 'Attempted to update a rate that is not editable: ' + @@ -230,7 +234,7 @@ function updateDraftContractRates( rateUpdate.formData, contract.stateCode, contract.stateNumber, - statePrograms, + statePrograms ) rateUpdates.update.push({ @@ -253,7 +257,6 @@ function updateDraftContractRates( ratePosition: thisPosition, }) } else { - // linked rates must exist and not be DRAFT const rateToLink = await store.findRateWithHistory( rateUpdate.rateID diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts index a59c56fec3..a3821e1c9c 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts @@ -169,6 +169,14 @@ describe('convertHealthPlanPackageRatesToDomain', () => { email: 'actuarycontact1@test.com', }, ], + addtlActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Additional Actuary Contact', + titleRole: 'Test Additional Actuary Contact', + email: 'additionalactuarycontact1@test.com', + }, + ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], }, @@ -199,6 +207,14 @@ describe('convertHealthPlanPackageRatesToDomain', () => { email: 'actuarycontact1@test.com', }, ], + addtlActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Additional Actuary Contact', + titleRole: 'Test Additional Actuary Contact', + email: 'additionalactuarycontact1@test.com', + }, + ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [ { @@ -217,15 +233,8 @@ describe('convertHealthPlanPackageRatesToDomain', () => { }, ], stateContacts: [], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', - addtlActuaryContacts: [ - { - actuarialFirm: 'DELOITTE', - name: 'Additional Actuary Contact', - titleRole: 'Test Actuary Contact', - email: 'additionalactuarycontact1@test.com', - }, - ], statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', } @@ -258,12 +267,11 @@ describe('convertHealthPlanPackageRatesToDomain', () => { ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], - // Additional actuaries from package should be in each rate now. addtlActuaryContacts: [ { actuarialFirm: 'DELOITTE', name: 'Additional Actuary Contact', - titleRole: 'Test Actuary Contact', + titleRole: 'Test Additional Actuary Contact', email: 'additionalactuarycontact1@test.com', }, ], @@ -308,7 +316,7 @@ describe('convertHealthPlanPackageRatesToDomain', () => { { actuarialFirm: 'DELOITTE', name: 'Additional Actuary Contact', - titleRole: 'Test Actuary Contact', + titleRole: 'Test Additional Actuary Contact', email: 'additionalactuarycontact1@test.com', }, ], diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts index 8a002e1c7b..01ef60f22b 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts @@ -132,10 +132,7 @@ const convertHealthPlanPackageRatesToDomain = async ( rateProgramIDs: hppRateFormData.rateProgramIDs, rateCertificationName: hppRateFormData.rateCertificationName, certifyingActuaryContacts: hppRateFormData.actuaryContacts, - // Frontend UI saves both these two values ar the contract level of the HPP type. Our new Contract type does - // not have these two fields, they are at the rate revision level. So, when converting we need to get the - // values from the HPP contract and set them into our rate. - addtlActuaryContacts: unlockedFormData.addtlActuaryContacts, + addtlActuaryContacts: hppRateFormData.addtlActuaryContacts, // toProtobuffer already does this, so we can directly set the value from the rate data. actuaryCommunicationPreference: hppRateFormData.actuaryCommunicationPreference, diff --git a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts index 4e3bf7500a..96fd7c69c4 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts @@ -259,6 +259,8 @@ describe(`Tests unlockHealthPlanPackage`, () => { email: 'en@example.com', actuarialFirm: 'MERCER', }, + ], + addtlActuaryContacts: [ { name: 'Enrico Soletzo 2', titleRole: 'person', @@ -383,8 +385,14 @@ describe(`Tests unlockHealthPlanPackage`, () => { finallySubmittedFormData.rateInfos[0].actuaryContacts.map( (c) => c.name ) - expect(actuariesInOrder).toEqual([ - 'Enrico Soletzo 1', + expect(actuariesInOrder).toEqual(['Enrico Soletzo 1']) + + // checks additional actuaries in order + const addtlActuariesInOrder = + finallySubmittedFormData.rateInfos[0].addtlActuaryContacts?.map( + (c) => c.name + ) + expect(addtlActuariesInOrder).toEqual([ 'Enrico Soletzo 2', 'Enrico Soletzo 3', ]) diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts index de41049f4c..6194647152 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts @@ -224,23 +224,28 @@ describe(`Tests UpdateHealthPlanFormData`, () => { // update that draft form data. const formData = Object.assign(latestFormData(createdDraft), { - addtlActuaryContacts: [ + rateInfos: [ { - name: 'additional actuary 1', - titleRole: 'additional actuary title 1', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - { - name: 'additional actuary 2', - titleRole: 'additional actuary title 2', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', + ...rate1, + addtlActuaryContacts: [ + { + name: 'additional actuary 1', + titleRole: 'additional actuary title 1', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + { + name: 'additional actuary 2', + titleRole: 'additional actuary title 2', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], }, + rate2, ], - rateInfos: [rate1, rate2], }) // convert to base64 proto @@ -284,6 +289,18 @@ describe(`Tests UpdateHealthPlanFormData`, () => { packageName: packageWithSharedRate2.packageName, }), ]), + addtlActuaryContacts: expect.arrayContaining([ + expect.objectContaining({ + name: 'additional actuary 1', + titleRole: 'additional actuary title 1', + email: 'additionalactuary1@example.com', + }), + expect.objectContaining({ + name: 'additional actuary 2', + titleRole: 'additional actuary title 2', + email: 'additionalactuary1@example.com', + }), + ]), }), expect.objectContaining({ ...rate2, diff --git a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts index 0c5d2fd120..9a70ff8408 100644 --- a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts +++ b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts @@ -212,6 +212,15 @@ function unlockedWithContacts(): UnlockedHealthPlanFormDataType { actuarialFirm: 'OTHER' as const, actuarialFirmOther: 'ACME', }, + ], + addtlActuaryContacts: [ + { + name: 'foo bar', + titleRole: 'manager', + email: 'soandso@example.com', + actuarialFirm: 'OTHER' as const, + actuarialFirmOther: 'ACME', + }, { name: 'Fine Bab', titleRole: 'supervisor', @@ -237,21 +246,7 @@ function unlockedWithContacts(): UnlockedHealthPlanFormDataType { email: 'lodar@example.com', }, ], - addtlActuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', @@ -318,6 +313,15 @@ function unlockedWithDocuments(): UnlockedHealthPlanFormDataType { ], supportingDocuments: [], actuaryContacts: [ + { + name: 'foo bar', + titleRole: 'manager', + email: 'soandso@example.com', + actuarialFirm: 'OTHER' as const, + actuarialFirmOther: 'ACME', + } + ], + addtlActuaryContacts: [ { name: 'foo bar', titleRole: 'manager', @@ -350,21 +354,7 @@ function unlockedWithDocuments(): UnlockedHealthPlanFormDataType { email: 'lodar@example.com', }, ], - addtlActuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', @@ -434,6 +424,15 @@ function unlockedWithFullRates(): UnlockedHealthPlanFormDataType { actuarialFirm: 'OTHER' as const, actuarialFirmOther: 'ACME', }, + ], + addtlActuaryContacts: [ + { + name: 'foo bar', + titleRole: 'manager', + email: 'soandso@example.com', + actuarialFirm: 'OTHER' as const, + actuarialFirmOther: 'ACME', + }, { name: 'Fine Bab', titleRole: 'supervisor', @@ -457,21 +456,7 @@ function unlockedWithFullRates(): UnlockedHealthPlanFormDataType { email: 'lodar@example.com', }, ], - addtlActuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', @@ -568,6 +553,15 @@ function unlockedWithFullContracts(): UnlockedHealthPlanFormDataType { actuarialFirm: 'OTHER' as const, actuarialFirmOther: 'ACME', }, + ], + addtlActuaryContacts: [ + { + name: 'foo bar', + titleRole: 'manager', + email: 'soandso@example.com', + actuarialFirm: 'OTHER' as const, + actuarialFirmOther: 'ACME', + }, { name: 'Fine Bab', titleRole: 'supervisor', @@ -591,21 +585,7 @@ function unlockedWithFullContracts(): UnlockedHealthPlanFormDataType { email: 'lodar@example.com', }, ], - addtlActuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', @@ -706,6 +686,15 @@ function unlockedWithALittleBitOfEverything(): UnlockedHealthPlanFormDataType { actuarialFirm: 'OTHER' as const, actuarialFirmOther: 'ACME', }, + ], + addtlActuaryContacts: [ + { + name: 'foo bar', + titleRole: 'manager', + email: 'soandso@example.com', + actuarialFirm: 'OTHER' as const, + actuarialFirmOther: 'ACME', + }, { name: 'Fine Bab', titleRole: 'supervisor', @@ -729,21 +718,7 @@ function unlockedWithALittleBitOfEverything(): UnlockedHealthPlanFormDataType { email: 'lodar@example.com', }, ], - addtlActuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', diff --git a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts index 67902cd997..2387666c6f 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts @@ -81,6 +81,7 @@ type RateInfoType = { rateProgramIDs?: string[] rateCertificationName?: string actuaryContacts: ActuaryContact[] + addtlActuaryContacts?: ActuaryContact[] actuaryCommunicationPreference?: ActuaryCommunicationType packagesWithSharedRateCerts?: SharedRateCertDisplay[] } diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts index ffaf3755f1..820d190615 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts @@ -338,6 +338,14 @@ function parseRateInfos( if (rateInfos.length > 0) { rateInfos.forEach((rateInfo) => { + + /** + * Not adding more to the proto schema, instead additional actuaries are added to the actuaryContacts array + * from index 1. + */ + const certifyingActuary = rateInfo?.actuaryContacts?.slice(0,1) + const additionalActuaries = rateInfo.actuaryContacts?.slice(1) + const rate: RecursivePartial = { id: rateInfo.id ?? undefined, rateAmendmentInfo: parseProtoRateAmendment( @@ -365,7 +373,10 @@ function parseRateInfos( rateInfo?.rateCertificationName ), actuaryContacts: parseActuaryContacts( - rateInfo?.actuaryContacts + certifyingActuary + ), + addtlActuaryContacts: parseActuaryContacts( + additionalActuaries ), actuaryCommunicationPreference: enumToDomain( mcreviewproto.ActuaryCommunicationType, diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts index 394f2d4715..2ed5041ecf 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts @@ -200,6 +200,15 @@ const toProtoBuffer = ( rateInfos: domainData.rateInfos && domainData.rateInfos.length ? domainData.rateInfos.map((rateInfo) => { + + /** + * Not adding more to the proto schema, we will combine certifying actuaries with additional actuaries + * in the same array, where certifying actuary is at index 0 and additional actuaries are from index + * 1 and beyond. + */ + const combinedActuaries = + rateInfo?.actuaryContacts.concat(rateInfo?.addtlActuaryContacts ?? []) ?? [] + return { id: rateInfo.id, rateType: domainEnumToProto( @@ -239,7 +248,11 @@ const toProtoBuffer = ( rateInfo.rateAmendmentInfo.effectiveDateEnd ), }, - actuaryContacts: rateInfo.actuaryContacts.map( + /** + * Not making more changes to the proto schema, instead additional actuaries are added to the + * actuaryContacts array from index 1. + */ + actuaryContacts: combinedActuaries.map( (actuaryContact) => { const firmType = domainEnumToProto( actuaryContact.actuarialFirm, @@ -267,24 +280,12 @@ const toProtoBuffer = ( } }) : undefined, - addtlActuaryContacts: domainData.addtlActuaryContacts.map( - (actuaryContact) => { - const firmType = domainEnumToProto( - actuaryContact.actuarialFirm, - mcreviewproto.ActuarialFirmType - ) - - return { - contact: { - name: actuaryContact.name, - titleRole: actuaryContact.titleRole, - email: actuaryContact.email, - }, - actuarialFirmType: firmType, - actuarialFirmOther: actuaryContact.actuarialFirmOther, - } - } - ), + /** + * This field is unused and will need cleaned up, additional actuaries have been moved to the rate level. + * Leaving this as am empty array to get linked rates feature work in without having to fix all the broken tests + * by removing this field. + */ + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: domainEnumToProto( domainData.addtlActuaryCommunicationPreference, mcreviewproto.ActuaryCommunicationType diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts index 466fe5adc1..6c8debd7ee 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts @@ -132,6 +132,7 @@ const rateInfosTypeSchema = z.object({ rateProgramIDs: z.array(z.string()), rateCertificationName: z.string().optional(), actuaryContacts: z.array(actuaryContactSchema), + addtlActuaryContacts: z.array(actuaryContactSchema).optional(), actuaryCommunicationPreference: actuaryCommunicationTypeSchema.optional(), packagesWithSharedRateCerts: z.array(sharedRateCertDisplay), }) diff --git a/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx b/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx index 57b55a960e..a2d9c24a06 100644 --- a/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx +++ b/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx @@ -190,19 +190,19 @@ export const UnlockSubmitModalV2 = ({ console.info('unlock rate not implemented yet') break - case 'SUBMIT_RATE' : + case 'SUBMIT_RATE': + console.info('submit rate not implemented yet') + break + case 'RESUBMIT_RATE': console.info('submit rate not implemented yet') break - case 'RESUBMIT_RATE' : - console.info('submit rate not implemented yet') - break case 'SUBMIT_CONTRACT': result = await submitMutationWrapperV2( submitContract, submissionData.id, unlockSubmitModalInput ) - break; + break case 'RESUBMIT_CONTRACT': result = await submitMutationWrapperV2( submitContract, diff --git a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.test.tsx index b6186352fb..fa99756daf 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.test.tsx @@ -123,7 +123,35 @@ describe('ContactsSummarySection', () => { }) it('does not include additional actuary contacts heading when this optional field is not provided', () => { - const mockSubmission = { ...draftSubmission, addtlActuaryContacts: [] } + const mockSubmission = mockContractAndRatesDraft({ + rateInfos: [ + { + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + rateDocuments: [], + supportingDocuments: [], + rateDateStart: new Date(), + rateDateEnd: new Date(), + rateDateCertified: new Date(), + rateAmendmentInfo: { + effectiveDateStart: new Date(), + effectiveDateEnd: new Date(), + }, + rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + actuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Actuary Contact 1', + titleRole: 'Test Actuary Contact 1', + email: 'actuarycontact1@test.com', + }, + ], + addtlActuaryContacts: [], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + packagesWithSharedRateCerts: [], + }, + ], + }) renderWithProviders( ) diff --git a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx index 05a3cb7714..4129295977 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx @@ -37,6 +37,16 @@ export const ContactsSummarySection = ({ }: ContactsSummarySectionProps): React.ReactElement => { const isSubmitted = submission.status === 'SUBMITTED' + // Combine additional actuaries from all rates. + const additionalActuaries: ActuaryContact[] = submission.rateInfos.flatMap( + (rate) => { + if (rate.addtlActuaryContacts?.length) { + return rate.addtlActuaryContacts + } + return [] + } + ) + return ( - {submission.addtlActuaryContacts.length > 0 && ( + {additionalActuaries.length > 0 && (
- {submission.addtlActuaryContacts.map( + {additionalActuaries.map( (actuaryContact, index) => ( { - - afterEach ( () => { + afterEach(() => { jest.clearAllMocks() }) it('renders documents without errors', async () => { @@ -422,7 +421,12 @@ describe('UploadedDocumentsTable', () => { />, { apolloProvider: { - mocks: [fetchCurrentUserMock({ user: mockValidCMSUser(), statusCode: 200 })], + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + ], }, featureFlags: { 'link-rates': false, @@ -504,7 +508,12 @@ describe('UploadedDocumentsTable', () => { />, { apolloProvider: { - mocks: [fetchCurrentUserMock({ user: mockValidCMSUser(), statusCode: 200 })], + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + ], }, featureFlags: { 'link-rates': true, @@ -545,7 +554,12 @@ describe('UploadedDocumentsTable', () => { />, { apolloProvider: { - mocks: [fetchCurrentUserMock({ user: mockValidStateUser(), statusCode: 200 })], + mocks: [ + fetchCurrentUserMock({ + user: mockValidStateUser(), + statusCode: 200, + }), + ], }, featureFlags: { 'link-rates': true, @@ -556,7 +570,7 @@ describe('UploadedDocumentsTable', () => { expect(await screen.findByTestId('tag')).toBeInTheDocument() }) - it('does not validations when hideDynamicFeedback is set to true',async() =>{ + it('does not validations when hideDynamicFeedback is set to true', async () => { const testDocuments = [ { s3URL: 's3://foo/bar/test-1', @@ -593,10 +607,12 @@ describe('UploadedDocumentsTable', () => { } ) await waitFor(() => { - expect(screen.queryByText(/Only one document is allowed/)).not.toBeInTheDocument() + expect( + screen.queryByText(/Only one document is allowed/) + ).not.toBeInTheDocument() }) }) - it('renders document validations if hideDynamicFeedback is false and too many documents uploaded',async() =>{ + it('renders document validations if hideDynamicFeedback is false and too many documents uploaded', async () => { const testDocuments = [ { s3URL: 's3://foo/bar/test-1', @@ -633,7 +649,9 @@ describe('UploadedDocumentsTable', () => { } ) await waitFor(() => { - expect(screen.queryByText(/Only one document is allowed/)).toBeInTheDocument() + expect( + screen.queryByText(/Only one document is allowed/) + ).toBeInTheDocument() }) }) }) diff --git a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx index 99575435b4..e593e95840 100644 --- a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx @@ -38,7 +38,6 @@ export type UploadedDocumentsTableProps = { isSupportingDocuments?: boolean // used to calculate empty state and styles around the secondary supporting docs tables - would be nice to remove this in favor of more domain agnostic prop such as 'emptyStateText' multipleDocumentsAllowed?: boolean // used to determined if we display validations based on doc list length documentCategory?: string // used to determine if we display document category column - } export const UploadedDocumentsTable = ({ @@ -86,7 +85,9 @@ export const UploadedDocumentsTable = ({ // show legacy shared rates across submissions (this is feature replaced by linked rates) // to cms users always when data available, to state users only when linked rates flag is off - const showLegacySharedRatesAcross = Boolean(packagesWithSharedRateCerts && packagesWithSharedRateCerts.length > 0) + const showLegacySharedRatesAcross = Boolean( + packagesWithSharedRateCerts && packagesWithSharedRateCerts.length > 0 + ) const borderTopGradientStyles = `borderTopLinearGradient ${styles.uploadedDocumentsTable}` const supportingDocsTopMarginStyles = isSupportingDocuments @@ -149,7 +150,7 @@ export const UploadedDocumentsTable = ({
{tableCaptionJSX}
- { hasMultipleDocs && !hideDynamicFeedback && ( + {hasMultipleDocs && !hideDynamicFeedback && ( {getIn(values, `${fieldNamePrefix}.actuarialFirm`) === diff --git a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.test.tsx b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.test.tsx index 45c4ac0faf..5f385578f0 100644 --- a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.test.tsx +++ b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.test.tsx @@ -79,58 +79,6 @@ describe('Contacts', () => { expect(optionalLabels).toHaveLength(0) }) - it('displays correct form guidance for contract and rates submission', async () => { - jest.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: jest.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: contractAndRatesWithEmptyContacts(), - } - }) - - renderWithProviders(, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - }) - - const requiredLabels = await screen.findAllByText('Required') - expect(requiredLabels).toHaveLength(2) - const optionalLabels = await screen.queryAllByText('Optional') - expect(optionalLabels).toHaveLength(0) - expect( - screen.getByText('Additional Actuary Contacts') - ).toBeInTheDocument() - expect( - screen.getByText( - 'Provide contact information for any additional actuaries who worked directly on this submission.' - ) - ).toBeInTheDocument() - expect( - screen.getByRole('button', { name: 'Add actuary contact' }) - ).toBeInTheDocument() - expect( - screen.getByText( - 'Communication preference between CMS Office of the Actuary (OACT) and all state’s actuaries (i.e. certifying actuaries and additional actuary contacts)' - ) - ).toBeInTheDocument() - expect( - screen.getByText( - 'OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.' - ) - ).toBeInTheDocument() - expect( - screen.getByText( - 'OACT can communicate directly with the state, and the state will relay all written communication to their actuaries and set up time for any potential verbal discussions.' - ) - ).toBeInTheDocument() - expect(screen.queryAllByTestId('actuary-contact')).toHaveLength(0) - }) - it('checks saved mocked state contacts correctly', async () => { jest.spyOn( useHealthPlanPackageForm, @@ -273,44 +221,6 @@ describe('Contacts', () => { }) }) - it('after "Add actuary contact" button click, it should focus on the field name of the new actuary contact', async () => { - const mock = contractAndRatesWithEmptyContacts() - jest.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: jest.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: mock, - } - }) - - renderWithProviders(, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - }) - - const addActuaryContactButton = screen.getByRole('button', { - name: 'Add actuary contact', - }) - - addActuaryContactButton.click() - - await waitFor(() => { - expect(screen.getByText('Add actuary contact')).toBeInTheDocument() - - expect(screen.getAllByLabelText('Name')).toHaveLength(2) - - const firstActuaryContactName = screen.getAllByLabelText('Name')[1] - - expect(firstActuaryContactName).toHaveValue('') - expect(firstActuaryContactName).toHaveFocus() - }) - }) - it('after state contact "Remove contact" button click, should focus on add new contact button', async () => { renderWithProviders(, { apolloProvider: { @@ -334,40 +244,6 @@ describe('Contacts', () => { expect(addStateContactButton).toHaveFocus() }) - it('after actuary contact "Remove contact" button click, should focus on add new actuary contact button', async () => { - jest.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: jest.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: contractAndRatesWithEmptyContacts(), - } - }) - - renderWithProviders(, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - }) - const addActuaryContactButton = screen.getByRole('button', { - name: 'Add actuary contact', - }) - await userEvent.click(addActuaryContactButton) - - expect( - screen.getByRole('button', { name: 'Remove contact' }) - ).toBeInTheDocument() - - await userEvent.click( - screen.getByRole('button', { name: 'Remove contact' }) - ) - - expect(addActuaryContactButton).toHaveFocus() - }) - it('when there are multiple state contacts, they should numbered', async () => { jest.spyOn( useHealthPlanPackageForm, @@ -396,167 +272,4 @@ describe('Contacts', () => { expect(screen.getByText('State contacts 2')).toBeInTheDocument() }) }) - - it('when there are multiple actuary contacts, they should numbered', async () => { - jest.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: jest.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: contractAndRatesWithEmptyContacts(), - } - }) - - renderWithProviders(, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - }) - const addActuaryContactButton = screen.getByRole('button', { - name: 'Add actuary contact', - }) - addActuaryContactButton.click() - addActuaryContactButton.click() - await waitFor(() => { - expect( - screen.getByText('Additional actuary contact 1') - ).toBeInTheDocument() - expect( - screen.getByText('Additional actuary contact 2') - ).toBeInTheDocument() - }) - }) - - /* This test is likely to time out if we use userEvent.type(). Converted to .paste() */ - it('when there are multiple state and actuary contacts, remove button works as expected', async () => { - jest.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: jest.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: contractAndRatesWithEmptyContacts(), - } - }) - - renderWithProviders(, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - }) - - screen.getAllByLabelText('Name')[0].focus() - await userEvent.paste('State Contact Person') - - screen.getAllByLabelText('Title/Role')[0].focus() - await userEvent.paste('State Contact Title') - - screen.getAllByLabelText('Email')[0].focus() - await userEvent.paste('statecontact@test.com') - - // add additional actuary contact - const addActuaryContactButton = screen.getByRole('button', { - name: 'Add actuary contact', - }) - addActuaryContactButton.click() - - await waitFor(() => { - expect( - screen.getByText('Additional actuary contact 1') - ).toBeInTheDocument() - }) - - // fill out additional actuary contact 1 - screen.getAllByLabelText('Name')[1].focus() - await userEvent.paste('Actuary Contact Person') - - screen.getAllByLabelText('Title/Role')[1].focus() - await userEvent.paste('Actuary Contact Title') - - screen.getAllByLabelText('Email')[1].focus() - await userEvent.paste('actuarycontact@test.com') - - await userEvent.click(screen.getAllByLabelText('Mercer')[0]) - - await userEvent.click( - screen.getByText( - `OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.` - ) - ) - - // Add additional state contact - await userEvent.click( - screen.getByRole('button', { - name: /Add another state contact/, - }) - ) - - screen.getAllByLabelText('Name')[1].focus() - await userEvent.paste('State Contact Person 2') - - screen.getAllByLabelText('Title/Role')[1].focus() - await userEvent.paste('State Contact Title 2') - - screen.getAllByLabelText('Email')[1].focus() - await userEvent.paste('statecontact2@test.com') - - expect(screen.queryAllByTestId('errorMessage')).toHaveLength(0) - - // Add additional actuary contact - addActuaryContactButton.click() - - screen.getAllByLabelText('Name')[1].focus() - await userEvent.paste('Actuary Contact Person 2') - - screen.getAllByLabelText('Title/Role')[1].focus() - await userEvent.paste('Actuary Contact Title 2') - - screen.getAllByLabelText('Email')[1].focus() - await userEvent.paste('actuarycontact2@test.com') - - await userEvent.click(screen.getAllByLabelText('Mercer')[1]) - - // Remove additional state contact - expect( - screen.getAllByRole('button', { name: /Remove contact/ }) - ).toHaveLength(3) // there are two remove contact buttons on screen, one for state one for actuary - await userEvent.click( - screen.getAllByRole('button', { name: /Remove contact/ })[0] - ) - - expect(screen.queryByText('State contact 2')).toBeNull() - expect( - screen.queryByText('Additional actuary contact 2') - ).toBeInTheDocument() - expect( - screen.queryByText('Additional actuary contact 1') - ).toBeInTheDocument() - - // Remove additional actuary contacts - expect( - screen.getAllByRole('button', { name: /Remove contact/ }) - ).toHaveLength(2) // there are 2 remove contact buttons on screen, for actuary - - //Remove actuary contact 2 - await userEvent.click( - screen.getAllByRole('button', { name: /Remove contact/ })[0] - ) - - expect(screen.queryByText('Additional actuary contact 2')).toBeNull() - expect( - screen.queryByText('Additional actuary contact 1') - ).toBeInTheDocument() - - //Remove actuary contact 1 - await userEvent.click( - screen.getAllByRole('button', { name: /Remove contact/ })[0] - ) - - expect(screen.queryByText('Additional actuary contact 1')).toBeNull() - }) }) diff --git a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx index 897616e98c..e7e58ce904 100644 --- a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx +++ b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx @@ -1,17 +1,11 @@ import React, { useEffect } from 'react' import * as Yup from 'yup' -import { - Form as UswdsForm, - FormGroup, - Fieldset, - Button, -} from '@trussworks/react-uswds' +import { Form as UswdsForm, Fieldset, Button } from '@trussworks/react-uswds' import { Formik, FormikErrors, FormikHelpers, FieldArray, - ErrorMessage, getIn, FieldArrayRenderProps, } from 'formik' @@ -19,17 +13,9 @@ import { useNavigate } from 'react-router-dom' import styles from '../StateSubmissionForm.module.scss' -import { - ActuaryCommunicationType, - ActuaryContact, - StateContact, -} from '../../../common-code/healthPlanFormDataType' +import { StateContact } from '../../../common-code/healthPlanFormDataType' -import { - ErrorSummary, - FieldRadio, - FieldTextInput, -} from '../../../components/Form' +import { ErrorSummary, FieldTextInput } from '../../../components/Form' import { useFocus } from '../../../hooks/useFocus' import { PageActions } from '../PageActions' @@ -37,7 +23,6 @@ import { activeFormPages, type HealthPlanFormPageProps, } from '../StateSubmissionForm' -import { ActuaryContactFields } from './ActuaryContactFields' import { RoutesRecord } from '../../../constants' import { DynamicStepIndicator, SectionCard } from '../../../components' import { FormContainer } from '../FormContainer' @@ -53,58 +38,6 @@ import { useErrorSummary } from '../../../hooks/useErrorSummary' export interface ContactsFormValues { stateContacts: StateContact[] - addtlActuaryContacts: ActuaryContact[] - actuaryCommunicationPreference: ActuaryCommunicationType | undefined -} - -const yupValidation = (submissionType: string) => { - const contactShape = { - stateContacts: Yup.array().of( - Yup.object().shape({ - name: Yup.string().required('You must provide a name'), - titleRole: Yup.string().required( - 'You must provide a title/role' - ), - email: Yup.string() - .email('You must enter a valid email address') - .trim() - .required('You must provide an email address'), - }) - ), - addtlActuaryContacts: Yup.array(), - actuaryCommunicationPreference: Yup.string().nullable(), - } - - if (submissionType !== 'CONTRACT_ONLY') { - contactShape.addtlActuaryContacts = Yup.array().of( - Yup.object().shape({ - name: Yup.string().required('You must provide a name'), - titleRole: Yup.string().required( - 'You must provide a title/role' - ), - email: Yup.string() - .email('You must enter a valid email address') - .trim() - .required('You must provide an email address'), - actuarialFirm: Yup.string() - .required('You must select an actuarial firm') - .nullable(), - actuarialFirmOther: Yup.string() - .when('actuarialFirm', { - is: 'OTHER', - then: Yup.string() - .required('You must enter a description') - .nullable(), - }) - .nullable(), - }) - ) - contactShape.actuaryCommunicationPreference = Yup.string().required( - 'You must select a communication preference' - ) - } - - return Yup.object().shape(contactShape) } type FormError = @@ -127,25 +60,6 @@ const flattenErrors = ( }) } - if ( - errors.addtlActuaryContacts && - Array.isArray(errors.addtlActuaryContacts) - ) { - errors.addtlActuaryContacts.forEach((contact, index) => { - if (!contact) return - - Object.entries(contact).forEach(([field, value]) => { - const errorKey = `addtlActuaryContacts.${index}.${field}` - flattened[errorKey] = value - }) - }) - - if (errors.actuaryCommunicationPreference) { - flattened['actuaryCommunicationPreference'] = - errors.actuaryCommunicationPreference - } - } - return flattened } @@ -176,8 +90,6 @@ const Contacts = ({ const [newStateContactButtonRef, setNewStateContactButtonFocus] = useFocus() // This ref.current is always the same element const newActuaryContactNameRef = React.useRef(null) - const [newActuaryContactButtonRef, setNewActuaryContactButtonFocus] = - useFocus() const navigate = useNavigate() @@ -208,9 +120,6 @@ const Contacts = ({ return const stateContacts = draftSubmission.stateContacts - const addtlActuaryContacts = draftSubmission.addtlActuaryContacts - const includeActuaryContacts = - draftSubmission.submissionType !== 'CONTRACT_ONLY' const emptyStateContact = { name: '', @@ -218,23 +127,12 @@ const Contacts = ({ email: '', } - const emptyActuaryContact = { - name: '', - titleRole: '', - email: '', - actuarialFirm: undefined, - actuarialFirmOther: '', - } - if (stateContacts.length === 0) { stateContacts.push(emptyStateContact) } const contactsInitialValues: ContactsFormValues = { stateContacts: stateContacts, - addtlActuaryContacts: addtlActuaryContacts, - actuaryCommunicationPreference: - draftSubmission?.addtlActuaryCommunicationPreference ?? undefined, } // Handler for Contacts legends so that contacts show up as @@ -258,11 +156,6 @@ const Contacts = ({ formikHelpers: FormikHelpers ) => { draftSubmission.stateContacts = values.stateContacts - if (includeActuaryContacts) { - draftSubmission.addtlActuaryContacts = values.addtlActuaryContacts - draftSubmission.addtlActuaryCommunicationPreference = - values.actuaryCommunicationPreference - } try { const updatedSubmission = await updateDraft(draftSubmission) @@ -286,7 +179,20 @@ const Contacts = ({ } } - const contactSchema = yupValidation(draftSubmission.submissionType) + const contactSchema = Yup.object().shape({ + stateContacts: Yup.array().of( + Yup.object().shape({ + name: Yup.string().required('You must provide a name'), + titleRole: Yup.string().required( + 'You must provide a title/role' + ), + email: Yup.string() + .email('You must enter a valid email address') + .trim() + .required('You must provide an email address'), + }) + ), + }) return ( <> @@ -448,7 +354,6 @@ const Contacts = ({ 0 && ( - - ) - )} - - - - )} - - - - - -
-

- Actuaries' communication - preference -

- - - Actuarial communication - preference - - -
- - Required - - {showFieldErrors( - `True` - ) && ( - - )} - - -
-
-
-
- - )} - { if (!dirty) { diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx index ee83c66ab9..8c595c25ea 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx @@ -113,7 +113,7 @@ describe('RateDetails', () => { screen.queryByText(/All fields are required/) ).not.toBeInTheDocument() const requiredLabels = await screen.findAllByText('Required') - expect(requiredLabels).toHaveLength(7) + expect(requiredLabels).toHaveLength(8) const optionalLabels = screen.queryAllByText('Optional') expect(optionalLabels).toHaveLength(1) }) @@ -280,7 +280,7 @@ describe('RateDetails', () => { // check for expected errors await waitFor(() => { - expect(screen.queryAllByTestId('errorMessage')).toHaveLength(7) + expect(screen.queryAllByTestId('errorMessage')).toHaveLength(8) expect( screen.queryAllByText('You must select a program') ).toHaveLength(2) @@ -1748,6 +1748,12 @@ const fillOutIndexRate = async (screen: Screen, index: number) => { await userEvent.paste(`actuarycontact${index}@test.com`) await userEvent.click(withinTargetRateCert.getByLabelText('Mercer')) + + await userEvent.click( + screen.getByText( + "OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions." + ) + ) } const fillOutFirstRate = async (screen: Screen) => { diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx index 36613add35..89eff57fdc 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx @@ -1,14 +1,28 @@ import React from 'react' -import { Form as UswdsForm } from '@trussworks/react-uswds' -import { FieldArray, FieldArrayRenderProps, Formik, FormikErrors } from 'formik' +import { Fieldset, FormGroup, Form as UswdsForm } from '@trussworks/react-uswds' +import { + FieldArray, + FieldArrayRenderProps, + Formik, + FormikErrors, + getIn, +} from 'formik' import { useNavigate } from 'react-router-dom' import { v4 as uuidv4 } from 'uuid' import styles from '../StateSubmissionForm.module.scss' -import { RateInfoType } from '../../../common-code/healthPlanFormDataType' +import { + ActuaryCommunicationType, + RateInfoType, +} from '../../../common-code/healthPlanFormDataType' -import { DynamicStepIndicator, ErrorSummary } from '../../../components' +import { + DynamicStepIndicator, + ErrorSummary, + FieldRadio, + PoliteErrorMessage, +} from '../../../components' import { formatFormDateForDomain } from '../../../formHelpers' import { RateDetailsFormSchema } from './RateDetailsSchema' import { PageActions } from '../PageActions' @@ -25,6 +39,7 @@ import { import { formatActuaryContactsForForm, + formatAddtlActuaryContactsForForm, formatDocumentsForDomain, formatDocumentsForForm, formatForForm, @@ -79,6 +94,9 @@ const generateRateCertFormValues = (params?: { actuaryContacts: formatActuaryContactsForForm( rateInfo?.actuaryContacts ), + addtlActuaryContacts: formatAddtlActuaryContactsForForm( + rateInfo?.addtlActuaryContacts + ), actuaryCommunicationPreference: rateInfo?.actuaryCommunicationPreference, packagesWithSharedRateCerts: @@ -95,6 +113,7 @@ const generateRateCertFormValues = (params?: { interface RateInfoArrayType { rateInfos: RateCertFormType[] + actuaryCommunicationPreference: ActuaryCommunicationType | undefined } export const rateErrorHandling = ( @@ -152,6 +171,10 @@ export const RateDetails = ({ generateRateCertFormValues({ rateInfo, getKey }) ) : [generateRateCertFormValues()], + actuaryCommunicationPreference: + draftSubmission.addtlActuaryCommunicationPreference + ? draftSubmission.addtlActuaryCommunicationPreference + : undefined, } const handleFormSubmit = async ( @@ -162,7 +185,8 @@ export const RateDetails = ({ redirectPath: string } ) => { - const { rateInfos } = form + const { rateInfos, actuaryCommunicationPreference } = form + if (options.shouldValidateDocuments) { const fileErrorsNeedAttention = rateInfos.some((rateInfo) => isLoadingOrHasFileErrors( @@ -178,7 +202,7 @@ export const RateDetails = ({ } } - const cleanedRateInfos = rateInfos.map((rateInfo) => { + draftSubmission.rateInfos = rateInfos.map((rateInfo) => { return { id: rateInfo.id, rateType: rateInfo.rateType, @@ -205,8 +229,8 @@ export const RateDetails = ({ : undefined, rateProgramIDs: rateInfo.rateProgramIDs, actuaryContacts: rateInfo.actuaryContacts, - actuaryCommunicationPreference: - rateInfo.actuaryCommunicationPreference, + addtlActuaryContacts: rateInfo.addtlActuaryContacts, + actuaryCommunicationPreference: actuaryCommunicationPreference, packagesWithSharedRateCerts: rateInfo.hasSharedRateCert === 'YES' ? rateInfo.packagesWithSharedRateCerts @@ -214,7 +238,8 @@ export const RateDetails = ({ } }) - draftSubmission.rateInfos = cleanedRateInfos + draftSubmission.addtlActuaryCommunicationPreference = + actuaryCommunicationPreference try { const updatedSubmission = await updateDraft(draftSubmission) @@ -239,6 +264,8 @@ export const RateDetails = ({ errors: FormikErrors ) => { const rateErrors = errors.rateInfos + const actuaryCommunicationPreference = + errors.actuaryCommunicationPreference const errorObject: { [field: string]: string } = {} if (rateErrors && Array.isArray(rateErrors)) { @@ -276,8 +303,15 @@ export const RateDetails = ({ }) } + if (actuaryCommunicationPreference) { + errorObject['actuaryCommunicationPreference'] = + actuaryCommunicationPreference + } + return errorObject } + const showFieldErrors = (error?: string | undefined) => + shouldValidate && Boolean(error) return ( <> @@ -295,16 +329,23 @@ export const RateDetails = ({ { - return handleFormSubmit({ rateInfos }, setSubmitting, { - shouldValidateDocuments: true, - redirectPath: `../contacts`, - }) + onSubmit={( + { rateInfos, actuaryCommunicationPreference }, + { setSubmitting } + ) => { + return handleFormSubmit( + { rateInfos, actuaryCommunicationPreference }, + setSubmitting, + { + shouldValidateDocuments: true, + redirectPath: `../contacts`, + } + ) }} validationSchema={rateDetailsFormSchema} > {({ - values: { rateInfos }, + values: { rateInfos, actuaryCommunicationPreference }, errors, dirty, handleSubmit, @@ -407,13 +448,82 @@ export const RateDetails = ({ )} + + +
+ + Required + + + Communication preference + between CMS Office of + the Actuary (OACT) and + all state’s actuaries + (i.e. certifying + actuaries and additional + actuary contacts) + + + {showFieldErrors( + errors.actuaryCommunicationPreference + ) && + getIn( + errors, + 'actuaryCommunicationPreference' + )} + + + +
+
+
{ const redirectPath = `../contract-details` if (dirty) { await handleFormSubmit( - { rateInfos }, + { + rateInfos, + actuaryCommunicationPreference, + }, setSubmitting, { shouldValidateDocuments: @@ -427,7 +537,10 @@ export const RateDetails = ({ }} saveAsDraftOnClick={async () => { await handleFormSubmit( - { rateInfos }, + { + rateInfos, + actuaryCommunicationPreference, + }, setSubmitting, { shouldValidateDocuments: diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.test.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.test.ts index b4736f5255..adbbed2281 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.test.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.test.ts @@ -13,7 +13,8 @@ describe('RateDetailsSchema', () => { supportingDocuments: [], ratePreviouslySubmitted: 'YES', } - ] + ], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY' } const res = await RateDetailsFormSchema({'link-rates': true}).validate(badRateRev, {abortEarly: false}) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts index 7f62352692..bcbb71fdd4 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts @@ -138,21 +138,47 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => .nullable(), }) ), - actuaryCommunicationPreference: Yup.string().optional(), + addtlActuaryContacts: Yup.array().of( + Yup.object().shape({ + name: Yup.string().required('You must provide a name'), + titleRole: Yup.string().required( + 'You must provide a title/role' + ), + email: Yup.string() + .email('You must enter a valid email address') + .required('You must provide an email address'), + actuarialFirm: Yup.string() + .required('You must select an actuarial firm') + .nullable(), + actuarialFirmOther: Yup.string() + .when('actuarialFirm', { + is: 'OTHER', + then: Yup.string() + .required('You must enter a description') + .nullable(), + }) + .nullable(), + }) + ), }) }) - const RateDetailsFormSchema = (activeFeatureFlags?: FeatureFlagSettings) => { return activeFeatureFlags?.['rate-edit-unlock'] || activeFeatureFlags?.['link-rates'] ? Yup.object().shape({ rateForms: Yup.array().of( SingleRateCertSchema(activeFeatureFlags || {}) ), + actuaryCommunicationPreference: Yup.string().required( + 'You must select a communication preference' + ) }): - Yup.object().shape({ + Yup.object().shape({ rateInfos: Yup.array().of( SingleRateCertSchema(activeFeatureFlags || {}) ), + actuaryCommunicationPreference: Yup.string().required( + 'You must select a communication preference' + ) }) } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx index 6895107404..7f68e9cada 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Button, DatePicker, @@ -31,13 +31,20 @@ import { } from '../../../components/FileUpload' import { useS3 } from '../../../contexts/S3Context' -import { FormikErrors, getIn, useFormikContext } from 'formik' +import { + FieldArray, + FieldArrayRenderProps, + FormikErrors, + getIn, + useFormikContext, +} from 'formik' import { ActuaryCommunicationType, SharedRateCertDisplay, } from '../../../common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType' import { ActuaryContactFields } from '../Contacts' import { PackagesWithSharedRates } from './PackagesWithSharedRates' +import { useFocus } from '../../../hooks' const isRateTypeEmpty = (values: RateCertFormType): boolean => values.rateType === undefined @@ -58,6 +65,7 @@ export type RateCertFormType = { rateDocuments: FileItemT[] supportingDocuments: FileItemT[] actuaryContacts: ActuaryContact[] + addtlActuaryContacts: ActuaryContact[] actuaryCommunicationPreference?: ActuaryCommunicationType packagesWithSharedRateCerts: SharedRateCertDisplay[] hasSharedRateCert?: 'YES' | 'NO' @@ -84,6 +92,14 @@ type SingleRateCertProps = { multiRatesConfig?: MultiRatesConfig // this is only passed in to enable displaying this rate within the multi-rates UI } +const emptyActuaryContact = { + name: '', + titleRole: '', + email: '', + actuarialFirm: undefined, + actuarialFirmOther: '', +} + const RateDatesErrorMessage = ({ startDate, endDate, @@ -125,6 +141,20 @@ export const SingleRateCert = ({ const fieldNamePrefix = `rateInfos.${index}` const rateCertNumber = index + 1 const { errors, setFieldValue } = useFormikContext() + const [focusNewActuaryContact, setFocusNewActuaryContact] = useState(false) + + const newActuaryContactNameRef = useRef(null) + const [newActuaryContactButtonRef, setNewActuaryContactButtonFocus] = + useFocus() + + useEffect(() => { + if (focusNewActuaryContact) { + newActuaryContactNameRef.current && + newActuaryContactNameRef.current.focus() + setFocusNewActuaryContact(false) + newActuaryContactNameRef.current = null + } + }, [focusNewActuaryContact]) const showFieldErrors = ( fieldName: keyof RateCertFormType @@ -550,6 +580,66 @@ export const SingleRateCert = ({ fieldNamePrefix={`${fieldNamePrefix}.actuaryContacts.0`} fieldSetLegend="Certifying Actuary" /> + + {({ remove, push }: FieldArrayRenderProps) => ( +
+ {rateInfo.addtlActuaryContacts.length > 0 && + rateInfo.addtlActuaryContacts.map( + (_actuaryContact, index) => ( +
+ + +
+ ) + )} + +
+ )} +
{index >= 1 && multiRatesConfig && ( + + ) + )} + + + )} + ) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts b/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts index 3d75e079e1..50e3630ff5 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts @@ -1,5 +1,6 @@ import { formatActuaryContactsForForm, + formatAddtlActuaryContactsForForm, formatDocumentsForForm, formatDocumentsForGQL, formatForForm, @@ -93,8 +94,8 @@ const convertGQLRateToRateForm = (getKey: S3ClientT['getKey'], rate?: Rate, pare actuaryContacts: formatActuaryContactsForForm( rateForm?.certifyingActuaryContacts ), - addtlActuaryContacts: formatActuaryContactsForForm( - rateForm?.certifyingActuaryContacts + addtlActuaryContacts: formatAddtlActuaryContactsForForm( + rateForm?.addtlActuaryContacts ), actuaryCommunicationPreference: rateForm?.actuaryCommunicationPreference?? undefined, diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx index c92768445f..1b52633f51 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx @@ -86,7 +86,7 @@ export const ContractDetailsSummarySectionV2 = ({ const ldClient = useLDClient() const { loggedInUser } = useAuth() const isSubmittedOrCMSUser = - contract.status === 'SUBMITTED' || loggedInUser?.role === 'CMS_USER' + contract.status === 'SUBMITTED' || loggedInUser?.role === 'CMS_USER' const isEditing = !isSubmittedOrCMSUser && editNavigateTo !== undefined const contractFormData = getVisibleLatestContractFormData( contract, diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx index 2e43a2331a..854fa4591c 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx @@ -76,7 +76,10 @@ export const ReviewSubmitV2 = (): React.ReactElement => { } const isStateUser = loggedInUser?.role === 'STATE_USER' - const contractFormData = getVisibleLatestContractFormData(contract, isStateUser) + const contractFormData = getVisibleLatestContractFormData( + contract, + isStateUser + ) if (!contractFormData) return const isContractActionAndRateCertification = diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx index fc64c2ab33..a8f77c85e1 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx @@ -33,7 +33,10 @@ export const StateSubmissionForm = (): React.ReactElement => { featureFlags.LINK_RATES.defaultValue ) return ( -
+
{ const soAndSo = screen.getAllByRole('link', { name: 'soandso@example.com', }) - expect(soAndSo).toHaveLength(3) + expect(soAndSo).toHaveLength(2) expect(soAndSo[0]).toHaveAttribute( 'href', 'mailto:soandso@example.com' @@ -1832,7 +1832,7 @@ describe('SubmissionSummary', () => { expect(screen.getByText('Certifying actuary')).toBeInTheDocument() expect( screen.getAllByText('Additional actuary contact') - ).toHaveLength(2) + ).toHaveLength(1) expect( screen.getByText( 'OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.' @@ -2181,7 +2181,7 @@ describe('SubmissionSummary', () => { const soAndSo = screen.getAllByRole('link', { name: 'soandso@example.com', }) - expect(soAndSo).toHaveLength(3) + expect(soAndSo).toHaveLength(2) expect(soAndSo[0]).toHaveAttribute( 'href', 'mailto:soandso@example.com' @@ -2199,7 +2199,7 @@ describe('SubmissionSummary', () => { expect(screen.getByText('Certifying actuary')).toBeInTheDocument() expect( screen.getAllByText('Additional actuary contact') - ).toHaveLength(2) + ).toHaveLength(1) expect( screen.getByText( 'OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.' diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index feaf5000fb..f8a7e15d2e 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -193,6 +193,14 @@ function mockContractAndRatesDraft( email: 'actuarycontact1@test.com', }, ], + addtlActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Additional Actuary Contact', + titleRole: 'Test Actuary Contact', + email: 'additionalactuarycontact1@test.com', + }, + ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], }, @@ -209,14 +217,7 @@ function mockContractAndRatesDraft( email: 'statecontact2@test.com', }, ], - addtlActuaryContacts: [ - { - actuarialFirm: 'DELOITTE', - name: 'Additional Actuary Contact', - titleRole: 'Test Actuary Contact', - email: 'additionalactuarycontact1@test.com', - }, - ], + addtlActuaryContacts: [], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', statutoryRegulatoryAttestation: false, statutoryRegulatoryAttestationDescription: 'No compliance', diff --git a/services/app-web/src/testHelpers/jestRateHelpers.tsx b/services/app-web/src/testHelpers/jestRateHelpers.tsx index f870e19d03..cecdac8baa 100644 --- a/services/app-web/src/testHelpers/jestRateHelpers.tsx +++ b/services/app-web/src/testHelpers/jestRateHelpers.tsx @@ -75,11 +75,15 @@ const fillOutIndexRate = async (screen: Screen, index: number) => { expect( withinTargetRateCert.queryByText('Date certified') ).toBeInTheDocument() - expect(withinTargetRateCert.queryByText('Name')).toBeInTheDocument() expect( - withinTargetRateCert.queryByText('Title/Role') + withinTargetRateCert.queryAllByText('Name')[0] + ).toBeInTheDocument() + expect( + withinTargetRateCert.queryAllByText('Title/Role')[0] + ).toBeInTheDocument() + expect( + withinTargetRateCert.queryAllByText('Email')[0] ).toBeInTheDocument() - expect(withinTargetRateCert.queryByText('Email')).toBeInTheDocument() }) const startDateInputs = withinTargetRateCert.getAllByLabelText('Start date') @@ -93,16 +97,16 @@ const fillOutIndexRate = async (screen: Screen, index: number) => { await userEvent.paste('12/01/2021') // fill out actuary contact - withinTargetRateCert.getByLabelText('Name').focus() + withinTargetRateCert.getAllByLabelText('Name')[0].focus() await userEvent.paste(`Actuary Contact Person ${index}`) - withinTargetRateCert.getByLabelText('Title/Role').focus() + withinTargetRateCert.getAllByLabelText('Title/Role')[0].focus() await userEvent.paste(`Actuary Contact Title ${index}`) - withinTargetRateCert.getByLabelText('Email').focus() + withinTargetRateCert.getAllByLabelText('Email')[0].focus() await userEvent.paste(`actuarycontact${index}@test.com`) - await userEvent.click(withinTargetRateCert.getByLabelText('Mercer')) + await userEvent.click(withinTargetRateCert.getAllByLabelText('Mercer')[0]) } const rateCertifications = (screen: Screen) => { diff --git a/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts b/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts index 08a10641f7..045b3cacbe 100644 --- a/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts +++ b/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts @@ -1,5 +1,3 @@ -import {aliasMutation, aliasQuery} from '../../utils/graphql-test-utils'; - describe('CMS user', () => { beforeEach(() => { cy.stubFeatureFlags() @@ -24,6 +22,14 @@ describe('CMS user', () => { cy.findAllByTestId('rate-certification-form').each((form) => cy.wrap(form).within(() => cy.fillOutNewRateCertification()) ) + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { @@ -31,7 +37,6 @@ describe('CMS user', () => { name: /Contacts/, }).should('exist') cy.fillOutStateContact() - cy.fillOutAdditionalActuaryContact() cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { @@ -251,6 +256,14 @@ describe('CMS user', () => { cy.fillOutNewRateCertification(); }) ) + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) cy.navigateContractRatesFormByButtonClick('CONTINUE') // fill out the rest of the form @@ -259,11 +272,6 @@ describe('CMS user', () => { name: /Contacts/, }).should('exist') cy.fillOutStateContact() - // There is a currently bug with actuary contacts with linked rates that makes us delete actuary contacts that aren't even filled out on this page - cy.findAllByRole('button', { name: 'Remove contact' }).last() - .should('exist') - .click() - cy.fillOutAdditionalActuaryContact(); cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { level: 2, diff --git a/services/cypress/integration/cmsWorkflow/viewSubmission.spec.ts b/services/cypress/integration/cmsWorkflow/viewSubmission.spec.ts index f3d4af6885..5c922395cc 100644 --- a/services/cypress/integration/cmsWorkflow/viewSubmission.spec.ts +++ b/services/cypress/integration/cmsWorkflow/viewSubmission.spec.ts @@ -15,6 +15,15 @@ describe('CMS user can view submission', () => { name: /Rate details/, }).should('exist') cy.fillOutNewRateCertification() + cy.fillOutAdditionalActuaryContact() + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { @@ -22,7 +31,6 @@ describe('CMS user can view submission', () => { name: /Contacts/, }).should('exist') cy.fillOutStateContact() - cy.fillOutAdditionalActuaryContact() cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { diff --git a/services/cypress/integration/stateWorkflow/dashboard/dashboard.spec.ts b/services/cypress/integration/stateWorkflow/dashboard/dashboard.spec.ts index 7d762a4c9e..f68f4dce00 100644 --- a/services/cypress/integration/stateWorkflow/dashboard/dashboard.spec.ts +++ b/services/cypress/integration/stateWorkflow/dashboard/dashboard.spec.ts @@ -22,6 +22,15 @@ describe('dashboard', () => { name: /Rate details/ }).should('exist') cy.fillOutNewRateCertification() + cy.fillOutAdditionalActuaryContact() + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { @@ -29,7 +38,6 @@ describe('dashboard', () => { name: /Contacts/, }).should('exist') cy.fillOutStateContact() - cy.fillOutAdditionalActuaryContact() cy.navigateFormByButtonClick('CONTINUE') cy.findByRole('heading', { level: 2, name: /Supporting documents/ }) diff --git a/services/cypress/integration/stateWorkflow/stateSubmissionForm/contacts.spec.ts b/services/cypress/integration/stateWorkflow/stateSubmissionForm/contacts.spec.ts index 052e16e692..74cbf05500 100644 --- a/services/cypress/integration/stateWorkflow/stateSubmissionForm/contacts.spec.ts +++ b/services/cypress/integration/stateWorkflow/stateSubmissionForm/contacts.spec.ts @@ -49,52 +49,19 @@ describe('contacts', () => { cy.navigateFormByButtonClick('BACK') cy.findByRole('heading', { level: 2, name: /Rate details/ }) cy.fillOutNewRateCertification() + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) cy.navigateFormByButtonClick('CONTINUE') // On contacts page, SAVE_DRAFT cy.findByRole('heading', { level: 2, name: /Contacts/ }) cy.fillOutStateContact() - - //Add two additional actuary contacts - cy.findByRole('button', { name: /Add actuary contact/ }).safeClick() - cy.findByRole('button', { name: /Add actuary contact/ }).safeClick() - - //Actuary contact should have 2 sets of actuary inputs - cy.findAllByTestId('actuary-contact').should('have.length', 2) - - //Fill out first actuary contact - cy.findAllByLabelText('Name') - .eq(1) - .click() - .type('Actuary Contact Person') - cy.findAllByLabelText('Title/Role') - .eq(1) - .type('Actuary Contact Title') - cy.findAllByLabelText('Email') - .eq(1) - .type('actuarycontact@example.com') - cy.findAllByLabelText('Mercer').eq(0).safeClick() - - //Fill out second actuary contact - cy.findAllByLabelText('Name') - .eq(2) - .click() - .type('Actuary Contact Person') - cy.findAllByLabelText('Title/Role') - .eq(2) - .type('Actuary Contact Title') - cy.findAllByLabelText('Email') - .eq(2) - .type('actuarycontact@example.com') - cy.findAllByLabelText('Mercer').eq(1).safeClick() - - // Actuary communication preference - cy.findByText( - `OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.` - ).click() - - cy.navigateFormByButtonClick('SAVE_DRAFT') - cy.findByRole('heading', { level: 1, name: /Submissions dashboard/ }) }) }) diff --git a/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts b/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts index eb28f423b7..6f0d6c7083 100644 --- a/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts +++ b/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts @@ -44,6 +44,15 @@ describe('rate details', () => { `/submissions/${draftSubmissionId}/edit/rate-details` ) + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) + cy.fillOutAmendmentToPriorRateCertification() /* Choose another submission that the rate cert was uploaded to, then check that your selection is still there when you come back */ @@ -98,6 +107,16 @@ describe('rate details', () => { }).click() cy.findAllByTestId('rate-certification-form').should('have.length', 3) + + cy.findByRole('radiogroup', { + name: /Actuaries' communication preference/ + }) + .should('exist') + .within(() => { + cy.findByText("OACT can communicate directly with the state's actuaries but should copy the state on all written communication and all appointments for verbal discussions.") + .click() + }) + //Fill out every rate certification form cy.findAllByTestId('rate-certification-form').each( (form, index, arr) => { @@ -105,12 +124,14 @@ describe('rate details', () => { //Fill out last rate certification as new rate if (index === arr.length - 1) { cy.fillOutNewRateCertification() + cy.fillOutAdditionalActuaryContact() } else { cy.fillOutAmendmentToPriorRateCertification(index) } }) } - ) + ) + // Navigate to contacts page by clicking continue cy.navigateFormByButtonClick('CONTINUE') @@ -118,9 +139,6 @@ describe('rate details', () => { //Fill out one state and one additional actuary contact cy.fillOutStateContact() - cy.findByRole('button', { name: /Add actuary contact/ }).safeClick() - cy.findAllByTestId('actuary-contact').should('have.length', 1) - cy.fillOutAdditionalActuaryContact() // Navigate back to rate details page cy.navigateFormByButtonClick('BACK') diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index a2c84f2843..f8ec396cd3 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -342,43 +342,42 @@ Cypress.Commands.add('fillOutNewRateCertification', () => { } + cy.findByText('New rate certification').click() + cy.findByText( + 'Certification of capitation rates specific to each rate cell' + ).click() + + cy.findAllByLabelText('Start date', {timeout: 2000}) + .parents() + .findByTestId('date-picker-external-input') + .type('02/29/2024') + cy.findAllByLabelText('End date') + .parents() + .findByTestId('date-picker-external-input') + .type('02/28/2025') + .blur() + cy.findByLabelText('Date certified').type('03/01/2024') + + cy.findByRole('combobox', { name: 'programs (required)' }).click({ + force: true, + }) + cy.findByText('PMAP').click() - cy.findByText('New rate certification').click() - cy.findByText( - 'Certification of capitation rates specific to each rate cell' - ).click() + //Fill out certifying actuary + cy.findAllByLabelText('Name').eq(0).click().type('Actuary Contact Person') + cy.findAllByLabelText('Title/Role').eq(0).type('Actuary Contact Title') + cy.findAllByLabelText('Email').eq(0).type('actuarycontact@example.com') + cy.findAllByLabelText('Mercer').eq(0).safeClick() - cy.findAllByLabelText('Start date', {timeout: 2000}) - .parents() - .findByTestId('date-picker-external-input') - .type('02/29/2024') - cy.findAllByLabelText('End date') - .parents() - .findByTestId('date-picker-external-input') - .type('02/28/2025') - .blur() - cy.findByLabelText('Date certified').type('03/01/2024') + // Upload a rate certification and rate supporting document + cy.findAllByTestId('file-input-input').each(fileInput => + cy.wrap(fileInput).attachFile('documents/trussel-guide.pdf') + ) - cy.findByRole('combobox', { name: 'programs (required)' }).click({ - force: true, + cy.verifyDocumentsHaveNoErrors() + cy.waitForDocumentsToLoad() + cy.findAllByTestId('errorMessage').should('have.length', 0) }) - cy.findByText('PMAP').click() - - //Fill out certifying actuary - cy.findAllByLabelText('Name').eq(0).click().type('Actuary Contact Person') - cy.findAllByLabelText('Title/Role').eq(0).type('Actuary Contact Title') - cy.findAllByLabelText('Email').eq(0).type('actuarycontact@example.com') - cy.findAllByLabelText('Mercer').eq(0).safeClick() - - // Upload a rate certification and rate supporting document - cy.findAllByTestId('file-input-input').each(fileInput => - cy.wrap(fileInput).attachFile('documents/trussel-guide.pdf') - ) - - cy.verifyDocumentsHaveNoErrors() - cy.waitForDocumentsToLoad() - cy.findAllByTestId('errorMessage').should('have.length', 0) -}) }) Cypress.Commands.add('fillOutLinkedRate', () => { @@ -469,23 +468,20 @@ Cypress.Commands.add('fillOutStateContact', () => { }) Cypress.Commands.add('fillOutAdditionalActuaryContact', () => { - // Must be on '/submissions/:id/edit/contacts' + // Must be on '/submissions/:id/edit/rate-details' // Must be a contract and rates submission - cy.findByRole('button', { name: 'Add actuary contact' }) + cy.findAllByRole('button', { name: 'Add a certifying actuary' }) .should('exist') + .eq(0) .click() - cy.findByText('Additional actuary contact 1').should('exist') - cy.findAllByLabelText('Name').eq(1).click().type('Actuary Contact Person') - cy.findAllByLabelText('Title/Role').eq(1).type('Actuary Contact Title') - cy.findAllByLabelText('Email').eq(1).type('actuarycontact@example.com') + cy.findByTestId('addtnl-actuary-contact').should('exist') + cy.findByTestId('addtlActuaryContacts.name').click().type('Actuary Contact Person') + cy.findByTestId('addtlActuaryContacts.titleRole').type('Actuary Contact Title') + cy.findByTestId('addtlActuaryContacts.email').type('actuarycontact@example.com') // Actuarial firm - cy.findAllByLabelText('Mercer').eq(0).safeClick() + cy.findByTestId('addtlActuaryContacts.mercer').safeClick() - // Actuary communication preference - cy.findByText( - `OACT can communicate directly with the state’s actuaries but should copy the state on all written communication and all appointments for verbal discussions.` - ).click() cy.findAllByTestId('errorMessage').should('have.length', 0) })