From 36e528dbbe0c239bb144ae79605c06d689bf228f Mon Sep 17 00:00:00 2001 From: pearl-truss <67110378+pearl-truss@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:26:47 -0500 Subject: [PATCH] MCR-3944: ReviewSubmit updated to use contract and rates (#2275) * wip: reviewSubmit added to V2 and contractAndRate mock created for using in storybook * fix tests for reviewsubmit -> RateDetailsSummarySection * add V2 version of component, storybook and test for SubmissionTypeSummarySection * wip push of contractdetailssummarysection and common code helpers * fixes 99% of the ContractDetailsSummarySectionV2 test * Add storybook file for contractdetails * fix contractdetails storybook * created component, test and storybook for ContactsSummarySection, updated graphql schema to match stateContacts for hpp with titleRole field * add uploads tables back to rateDetailsSummary add functionality for draft and submitted rates * wip integrating page * update file structure so reviewsubmitV2 related files are under reviewsubmit * code clean up in tests * fixed issue with fetchContract not being passed the createdAt and updatedAt fields from prisma and the resolver * remove unneeded console logs * resolve local test error * code cleanup, pr fixes * fix test * fix ui bug * additional PR fixes --- .../contractAndRates/contractTypes.ts | 2 + .../src/mutations/submitRate.graphql | 2 +- .../src/mutations/unlockRate.graphql | 8 +- .../src/queries/fetchContract.graphql | 118 +-- .../app-graphql/src/queries/fetchRate.graphql | 10 +- .../src/queries/indexRates.graphql | 2 +- services/app-graphql/src/schema.graphql | 8 +- .../app-web/src/common-code/ContractType.ts | 50 + .../src/common-code/ContractTypeProvisions.ts | 168 +++ .../DataDetailContactField.tsx | 4 +- .../UploadedDocumentsTable.tsx | 8 +- services/app-web/src/index.tsx | 23 +- .../ContactsSummarySectionV2.stories.tsx | 35 + .../ContactsSummarySectionV2.test.tsx | 141 +++ .../ReviewSubmit/ContactsSummarySectionV2.tsx | 145 +++ ...ontractDetailsSummarySectionV2.stories.tsx | 43 + .../ContractDetailsSummarySectionV2.test.tsx | 977 ++++++++++++++++++ .../ContractDetailsSummarySectionV2.tsx | 336 ++++++ .../RateDetailsSummarySectionV2.stories.tsx | 46 + .../RateDetailsSummarySectionV2.test.tsx | 786 ++++++++++++++ .../RateDetailsSummarySectionV2.tsx | 320 ++++++ .../V2/ReviewSubmit/ReviewSubmitV2.test.tsx | 148 +++ .../V2/ReviewSubmit/ReviewSubmitV2.tsx | 187 ++++ ...SubmissionTypeSummarySectionV2.stories.tsx | 38 + .../SubmissionTypeSummarySectionV2.test.tsx | 211 ++++ .../SubmissionTypeSummarySectionV2.tsx | 142 +++ .../StateSubmission/StateSubmissionForm.tsx | 5 +- .../apolloMocks/contractGQLMock.ts | 34 + .../apolloMocks/contractPackageDataMock.ts | 167 ++- .../apolloMocks/healthPlanFormDataMock.ts | 2 +- .../src/testHelpers/apolloMocks/index.ts | 7 +- .../testHelpers/apolloMocks/rateDataMock.ts | 2 +- 32 files changed, 4038 insertions(+), 137 deletions(-) create mode 100644 services/app-web/src/common-code/ContractType.ts create mode 100644 services/app-web/src/common-code/ContractTypeProvisions.ts create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.stories.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.test.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.stories.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.stories.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.test.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.stories.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.test.tsx create mode 100644 services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx create mode 100644 services/app-web/src/testHelpers/apolloMocks/contractGQLMock.ts diff --git a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts index 1d0196b732..98708bee9a 100644 --- a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts @@ -12,6 +12,8 @@ const contractSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), status: statusSchema, + createdAt: z.date(), + updatedAt: z.date(), stateCode: z.string(), mccrsID: z.string().optional(), stateNumber: z.number().min(1), diff --git a/services/app-graphql/src/mutations/submitRate.graphql b/services/app-graphql/src/mutations/submitRate.graphql index 96c3de8365..821b611bc6 100644 --- a/services/app-graphql/src/mutations/submitRate.graphql +++ b/services/app-graphql/src/mutations/submitRate.graphql @@ -101,7 +101,7 @@ mutation submitRate($input: SubmitRateInput!) { submissionDescription stateContacts { name, - title + titleRole email } supportingDocuments { diff --git a/services/app-graphql/src/mutations/unlockRate.graphql b/services/app-graphql/src/mutations/unlockRate.graphql index e03faeb404..01472d638a 100644 --- a/services/app-graphql/src/mutations/unlockRate.graphql +++ b/services/app-graphql/src/mutations/unlockRate.graphql @@ -1,4 +1,4 @@ -fragment rateRevisionFragment on RateRevision { +fragment rateRevisionFragmentForUnlock on RateRevision { id createdAt updatedAt @@ -82,7 +82,7 @@ fragment rateRevisionFragment on RateRevision { submissionDescription stateContacts { name, - title + titleRole email } supportingDocuments { @@ -143,11 +143,11 @@ mutation unlockRate($input: UnlockRateInput!) { initiallySubmittedAt draftRevision { - ...rateRevisionFragment + ...rateRevisionFragmentForUnlock } revisions { - ...rateRevisionFragment + ...rateRevisionFragmentForUnlock } } } diff --git a/services/app-graphql/src/queries/fetchContract.graphql b/services/app-graphql/src/queries/fetchContract.graphql index 2afbd48ef6..cd9e5bc1bd 100644 --- a/services/app-graphql/src/queries/fetchContract.graphql +++ b/services/app-graphql/src/queries/fetchContract.graphql @@ -1,21 +1,7 @@ query fetchContract($input: FetchContractInput!) { fetchContract(input: $input) { contract { - id - status - initiallySubmittedAt - stateCode - state { - code - name - programs { - id - name - fullName - } - } - - stateNumber + ...contractFields draftRevision { ...contractRevisionFragment @@ -25,11 +11,11 @@ query fetchContract($input: FetchContractInput!) { ...rateFields draftRevision { - ...rateRevisionFragment + ...rateRevisionFragmentForFetchContract } revisions { - ...rateRevisionFragment + ...rateRevisionFragmentForFetchContract } } @@ -40,6 +26,26 @@ query fetchContract($input: FetchContractInput!) { } } +fragment contractFields on Contract { + id + status + createdAt + updatedAt + initiallySubmittedAt + stateCode + state { + code + name + programs { + id + name + fullName + } + } + + stateNumber +} + fragment rateFields on Rate { id createdAt @@ -59,7 +65,7 @@ fragment rateFields on Rate { initiallySubmittedAt } -fragment rateRevisionFragment on RateRevision { +fragment rateRevisionFragmentForFetchContract on RateRevision { id createdAt updatedAt @@ -143,7 +149,7 @@ fragment rateRevisionFragment on RateRevision { submissionDescription stateContacts { name - title + titleRole email } supportingDocuments { @@ -194,7 +200,7 @@ fragment contractFormDataFragment on ContractFormData { stateContacts { name - title + titleRole email } @@ -236,50 +242,6 @@ fragment contractFormDataFragment on ContractFormData { modifiedNonRiskPaymentArrangements } -fragment rateFormDataFragment on RateFormData { - rateType - rateCapitationType - rateDocuments { - name - s3URL - sha256 - } - supportingDocuments { - name - s3URL - sha256 - } - rateDateStart - rateDateEnd - rateDateCertified - amendmentEffectiveDateStart - amendmentEffectiveDateEnd - rateProgramIDs - rateCertificationName - certifyingActuaryContacts { - id - name - titleRole - email - actuarialFirm - actuarialFirmOther - } - addtlActuaryContacts { - id - name - titleRole - email - actuarialFirm - actuarialFirmOther - } - actuaryCommunicationPreference - packagesWithSharedRateCerts { - packageName - packageId - packageStatus - } -} - fragment contractRevisionFragment on ContractRevision { id createdAt @@ -316,38 +278,16 @@ fragment packageSubmissionsFragment on ContractPackageSubmission { ...contractRevisionFragment } rateRevisions { - ...rateRevisionFragment + ...rateRevisionFragmentForFetchContract } } fragment submittableRevisionsFields on SubmittableRevision { ... on ContractRevision { - id - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragment - } + ...contractRevisionFragment } ... on RateRevision { - id - createdAt - updatedAt - unlockInfo { - ...updateInformationFields - } - submitInfo { - ...updateInformationFields - } - formData { - ...rateFormDataFragment - } + ...rateRevisionFragmentForFetchContract } } diff --git a/services/app-graphql/src/queries/fetchRate.graphql b/services/app-graphql/src/queries/fetchRate.graphql index 135daf1127..d14fcee1e3 100644 --- a/services/app-graphql/src/queries/fetchRate.graphql +++ b/services/app-graphql/src/queries/fetchRate.graphql @@ -1,4 +1,4 @@ -fragment rateRevisionFragment on RateRevision { +fragment rateRevisionFragmentForFetchRate on RateRevision { id createdAt updatedAt @@ -81,8 +81,8 @@ fragment rateRevisionFragment on RateRevision { riskBasedContract submissionDescription stateContacts { - name - title + name, + titleRole email } supportingDocuments { @@ -147,11 +147,11 @@ query fetchRate($input: FetchRateInput!) { ...rateFields draftRevision { - ...rateRevisionFragment + ...rateRevisionFragmentForFetchRate } revisions { - ...rateRevisionFragment + ...rateRevisionFragmentForFetchRate } } } diff --git a/services/app-graphql/src/queries/indexRates.graphql b/services/app-graphql/src/queries/indexRates.graphql index a708d93831..bbea7e77cc 100644 --- a/services/app-graphql/src/queries/indexRates.graphql +++ b/services/app-graphql/src/queries/indexRates.graphql @@ -159,7 +159,7 @@ query indexRates { submissionDescription stateContacts { name, - title + titleRole email } supportingDocuments { diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index 024fe9b0ea..31d5b24c64 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -876,7 +876,7 @@ enum FederalAuthority { "Contact information for contacting states regarding their submission" type StateContact { name: String - title: String + titleRole: String email: String } @@ -993,7 +993,11 @@ type ContractFormData { "If contract includes modifications to the length of the contract period" modifiedLengthOfContract: Boolean "If contract includes modifications to the non-risk payment arrangements" - modifiedNonRiskPaymentArrangements: Boolean + modifiedNonRiskPaymentArrangements: Boolean, + "If contract has statutory regulatory attestation" + statutoryRegulatoryAttestation: Boolean, + "Description provided for if contract has statutory regulatory attestation" + statutoryRegulatoryAttestationDescription: String } "Either new capitation rates (NEW) or updates to previously certified capitation rates (AMENDMENT)" diff --git a/services/app-web/src/common-code/ContractType.ts b/services/app-web/src/common-code/ContractType.ts new file mode 100644 index 0000000000..9c797a4952 --- /dev/null +++ b/services/app-web/src/common-code/ContractType.ts @@ -0,0 +1,50 @@ +import { Contract, ContractRevision } from '../gen/gqlClient' + +const getContractRev = (contract: Contract): ContractRevision => { + if (contract.draftRevision) { + return contract.draftRevision + } else { + return contract.packageSubmissions[0].contractRevision + } +} +const isContractOnly = (contract: Contract): boolean => { + const contractRev = getContractRev(contract) + return contractRev.formData.submissionType === 'CONTRACT_ONLY' +} + + +const isBaseContract = (contract: Contract): boolean => { + const contractRev = getContractRev(contract) + return contractRev.formData.contractType === 'BASE' +} + +const isContractAmendment = (contract: Contract): boolean => { + const contractRev = getContractRev(contract) + return contractRev.formData.contractType === 'AMENDMENT' +} + +const isCHIPOnly = (contract: Contract): boolean => { + const contractRev = getContractRev(contract) + return contractRev.formData.populationCovered === 'CHIP' +} + +const isContractAndRates = (contract: Contract): boolean => { + const contractRev = getContractRev(contract) + return contractRev.formData.submissionType === 'CONTRACT_AND_RATES' +} + +const isContractWithProvisions = (contract: Contract): boolean => + isContractAmendment(contract) || (isBaseContract(contract) && !isCHIPOnly(contract)) + +const isSubmitted = (contract: Contract): boolean => + contract.status === 'SUBMITTED' + +export { + isContractWithProvisions, + isBaseContract, + isContractAmendment, + isCHIPOnly, + isContractOnly, + isContractAndRates, + isSubmitted, +} diff --git a/services/app-web/src/common-code/ContractTypeProvisions.ts b/services/app-web/src/common-code/ContractTypeProvisions.ts new file mode 100644 index 0000000000..da16b3a56e --- /dev/null +++ b/services/app-web/src/common-code/ContractTypeProvisions.ts @@ -0,0 +1,168 @@ +import { + ModifiedProvisionsAmendmentRecord, + ModifiedProvisionsBaseContractRecord, + ModifiedProvisionsCHIPRecord, +} from '../constants/modifiedProvisions' +import { Contract } from '../gen/gqlClient' +import { + CHIPProvisionType, + MedicaidBaseProvisionType, + MedicaidAmendmentProvisionType, + provisionCHIPKeys, + modifiedProvisionMedicaidBaseKeys, + modifiedProvisionMedicaidAmendmentKeys, + GeneralizedProvisionType, + isCHIPProvision, + isMedicaidAmendmentProvision, + isMedicaidBaseProvision, +} from './healthPlanFormDataType/ModifiedProvisions' +import { + isBaseContract, + isCHIPOnly, + isContractAmendment, + isContractWithProvisions, +} from './ContractType' + +/* + Each provision key represents a Yes/No question asked on Contract Details. + This is a set of helper functions that each take in a submission and return provisions related data. + + There are currently three distrinct variants of the provisions: + 1. For CHIP amendment + 2. For non CHIP base contract + 3. For non CHIP contract amendment + + See also ModifiedProvisions.ts +*/ + +// Returns the list of provision keys that apply for given submission variant +const generateApplicableProvisionsList = ( + draftSubmission: Contract +): + | CHIPProvisionType[] + | MedicaidBaseProvisionType[] + | MedicaidAmendmentProvisionType[] => { + if (isCHIPOnly(draftSubmission)) { + return isContractAmendment(draftSubmission) + ? (provisionCHIPKeys as unknown as CHIPProvisionType[]) + : [] // there are no applicable provisions for CHIP base contract + } else if (isBaseContract(draftSubmission)) { + return modifiedProvisionMedicaidBaseKeys as unknown as MedicaidBaseProvisionType[] + } else { + return modifiedProvisionMedicaidAmendmentKeys as unknown as MedicaidAmendmentProvisionType[] + } +} + +// Returns user-friendly label text for the provision based on the given submission variant +const generateProvisionLabel = ( + draftSubmission: Contract, + provision: GeneralizedProvisionType +): string => { + if (isCHIPOnly(draftSubmission) && isCHIPProvision(provision)) { + return ModifiedProvisionsCHIPRecord[provision] + } else if ( + isBaseContract(draftSubmission) && + isMedicaidBaseProvision(provision) + ) { + return ModifiedProvisionsBaseContractRecord[provision] + } else if ( + isContractAmendment(draftSubmission) && + isMedicaidAmendmentProvision(provision) + ) { + return ModifiedProvisionsAmendmentRecord[provision] + } else { + console.warn('Coding Error: This is a fallback case and is unexpected.') + return 'Invalid Provision' + } +} + +/* + Returns two lists of provisions keys sorted by whether they are set true/false + This function also quietly discard keys from the submission's own provisions list that are not valid for the current variant. + That functionality needed for unlocked contracts which can be edited in a non-linear fashion) +*/ +const sortModifiedProvisions = ( + contract: Contract +): [GeneralizedProvisionType[], GeneralizedProvisionType[]] => { + const contractFormData = contract.draftRevision?.formData || contract.packageSubmissions[0].contractRevision.formData + const initialProvisions = { + inLieuServicesAndSettings: contractFormData.inLieuServicesAndSettings, + modifiedBenefitsProvided: contractFormData.modifiedBenefitsProvided, + modifiedGeoAreaServed: contractFormData.modifiedGeoAreaServed, + modifiedMedicaidBeneficiaries: contractFormData.modifiedMedicaidBeneficiaries, + modifiedRiskSharingStrategy: contractFormData.modifiedRiskSharingStrategy, + modifiedIncentiveArrangements: contractFormData.modifiedIncentiveArrangements, + modifiedWitholdAgreements: contractFormData.modifiedWitholdAgreements, + modifiedStateDirectedPayments: contractFormData.modifiedStateDirectedPayments, + modifiedPassThroughPayments: contractFormData.modifiedPassThroughPayments, + modifiedPaymentsForMentalDiseaseInstitutions: contractFormData.modifiedPaymentsForMentalDiseaseInstitutions, + modifiedMedicalLossRatioStandards: contractFormData.modifiedMedicalLossRatioStandards, + modifiedOtherFinancialPaymentIncentive: contractFormData.modifiedOtherFinancialPaymentIncentive, + modifiedEnrollmentProcess: contractFormData.modifiedEnrollmentProcess, + modifiedGrevienceAndAppeal: contractFormData.modifiedGrevienceAndAppeal, + modifiedNetworkAdequacyStandards: contractFormData.modifiedNetworkAdequacyStandards, + modifiedLengthOfContract: contractFormData.modifiedLengthOfContract, + modifiedNonRiskPaymentArrangements: contractFormData.modifiedNonRiskPaymentArrangements, + statutoryRegulatoryAttestation: contractFormData.statutoryRegulatoryAttestation, + statutoryRegulatoryAttestationDescription: contractFormData.statutoryRegulatoryAttestationDescription + } + const hasInitialProvisions = Object.values(initialProvisions).some((val) => val !== undefined) + const modifiedProvisions: GeneralizedProvisionType[] = [] + const unmodifiedProvisions: GeneralizedProvisionType[] = [] + + if (hasInitialProvisions && isContractWithProvisions(contract)) { + const applicableProvisions = + generateApplicableProvisionsList(contract) + + for (const provisionKey of applicableProvisions) { + const value = initialProvisions[provisionKey] + if (value === true) { + modifiedProvisions.push(provisionKey) + } else if (value === false) { + unmodifiedProvisions.push(provisionKey) + } + } + } + + return [modifiedProvisions, unmodifiedProvisions] +} + +/* + Returns boolean for weher a submission variant is missing required provisions + This is used to determine if we display the missing data warning on review and submit +*/ +const isMissingProvisions = (contract: Contract): boolean => { + const requiredProvisions = generateApplicableProvisionsList(contract) + const [modifiedProvisions, unmodifiedProvisions] = + sortModifiedProvisions(contract) + + return ( + modifiedProvisions.length + unmodifiedProvisions.length < + requiredProvisions.length + ) +} + +/* + Returns lang string dictionary for variant +*/ +const getProvisionDictionary = ( + contract: Contract +): + | typeof ModifiedProvisionsCHIPRecord + | typeof ModifiedProvisionsBaseContractRecord + | typeof ModifiedProvisionsAmendmentRecord => { + if (isCHIPOnly(contract)) { + return ModifiedProvisionsCHIPRecord + } else if (isBaseContract(contract)) { + return ModifiedProvisionsBaseContractRecord + } else { + return ModifiedProvisionsAmendmentRecord + } +} +export { + getProvisionDictionary, + sortModifiedProvisions, + generateApplicableProvisionsList, + generateProvisionLabel, + isMissingProvisions, +} diff --git a/services/app-web/src/components/DataDetail/DataDetailContactField/DataDetailContactField.tsx b/services/app-web/src/components/DataDetail/DataDetailContactField/DataDetailContactField.tsx index 3b524904c5..9ce5d1bc5e 100644 --- a/services/app-web/src/components/DataDetail/DataDetailContactField/DataDetailContactField.tsx +++ b/services/app-web/src/components/DataDetail/DataDetailContactField/DataDetailContactField.tsx @@ -5,9 +5,9 @@ import { } from '../../../common-code/healthPlanFormDataType' import { getActuaryFirm } from '../../SubmissionSummarySection' import { DataDetailMissingField } from '../DataDetailMissingField' -import { ActuaryContact as GQLActuaryContact } from '../../../gen/gqlClient' +import { ActuaryContact as GQLActuaryContact, StateContact as GQLStateContact } from '../../../gen/gqlClient' -type Contact = ActuaryContact | StateContact | GQLActuaryContact +type Contact = ActuaryContact | GQLActuaryContact | StateContact | GQLStateContact function isCertainActuaryContact(contact: Contact): contact is ActuaryContact { return (contact as ActuaryContact).actuarialFirm !== undefined } diff --git a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx index 9fda3b2252..072eed956c 100644 --- a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.tsx @@ -92,7 +92,7 @@ export const UploadedDocumentsTable = ({ Edit {caption} @@ -119,7 +119,7 @@ export const UploadedDocumentsTable = ({ if (refreshedDocs.length === 0) { return (
- {tableCaptionJSX} +
{tableCaptionJSX}

@@ -137,7 +137,9 @@ export const UploadedDocumentsTable = ({ className={`${borderTopGradientStyles} ${supportingDocsTopMarginStyles}`} > - {tableCaptionJSX} +

+ {tableCaptionJSX} +
{!multipleDocumentsAllowed && documents.length > 1 && !isSubmitted && ( diff --git a/services/app-web/src/index.tsx b/services/app-web/src/index.tsx index 33c55cd844..d4b4fbfeeb 100644 --- a/services/app-web/src/index.tsx +++ b/services/app-web/src/index.tsx @@ -1,6 +1,11 @@ import React from 'react' import { createRoot } from 'react-dom/client' -import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client' +import { + ApolloClient, + InMemoryCache, + HttpLink, + DefaultOptions, +} from '@apollo/client' import { Amplify } from 'aws-amplify' import { loader } from 'graphql.macro' @@ -57,17 +62,23 @@ Amplify.configure({ const authMode = process.env.REACT_APP_AUTH_MODE assertIsAuthMode(authMode) +const cache = new InMemoryCache() +const defaultOptions: DefaultOptions = { + watchQuery: { + fetchPolicy: 'network-only', + }, + query: { + fetchPolicy: 'network-only', + }, +} const apolloClient = new ApolloClient({ link: new HttpLink({ uri: '/graphql', fetch: authMode === 'LOCAL' ? localGQLFetch : fakeAmplifyFetch, }), - cache: new InMemoryCache({ - possibleTypes: { - Submission: ['DraftSubmission', 'StateSubmission'], - }, - }), + cache, + defaultOptions, typeDefs: gqlSchema, }) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.stories.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.stories.tsx new file mode 100644 index 0000000000..c4e532cb2e --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.stories.tsx @@ -0,0 +1,35 @@ +import { Story } from '@storybook/react' +import ProvidersDecorator from '../../../../../../.storybook/providersDecorator' +import { + ContactsSummarySectionProps, + ContactsSummarySection, +} from './ContactsSummarySectionV2' +import { mockContractPackageDraft } from '../../../../../testHelpers/apolloMocks' + +export default { + title: 'Components/SubmissionSummary/ContactsSummarySection/V2', + component: ContactsSummarySection, + parameters: { + componentSubtitle: + 'ContactsSummarySection displays the Contacts data for a Draft or State Submission', + }, +} + +const Template: Story = (args) => ( + +) + +export const WithAction = Template.bind({}) +WithAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithAction.args = { + contract: mockContractPackageDraft(), + editNavigateTo: 'contract-details', +} + +export const WithoutAction = Template.bind({}) +WithoutAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithoutAction.args = { + contract: mockContractPackageDraft(), +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.test.tsx new file mode 100644 index 0000000000..75b7d178f2 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.test.tsx @@ -0,0 +1,141 @@ +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../../testHelpers/jestHelpers' +import { ContactsSummarySection } from './ContactsSummarySectionV2' +import { + mockContractPackageDraft, + mockContractPackageSubmitted, +} from '../../../../../testHelpers/apolloMocks' + +describe('ContactsSummarySection', () => { + const draftSubmission = mockContractPackageDraft() + const stateSubmission = mockContractPackageSubmitted() + afterEach(() => jest.clearAllMocks()) + + it('can render draft submission without errors', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'State contacts', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('heading', { + level: 2, + name: 'Additional actuary contacts', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('link', { name: 'Edit State contacts' }) + ).toHaveAttribute('href', '/contacts') + }) + + it('can render state submission without errors', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'State contacts', + }) + ).toBeInTheDocument() + expect(screen.queryByText('Edit')).not.toBeInTheDocument() + }) + + it('can render all state and actuary contact fields', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'State contacts', + }) + ).toBeInTheDocument() + expect(screen.queryByText('Contact 1')).toBeInTheDocument() + // expect(screen.queryByText('State Contact 1')).toBeInTheDocument() + // expect(screen.queryByText('Test State Contact 1')).toBeInTheDocument() + expect( + screen.getByRole('heading', { + level: 2, + name: 'Additional actuary contacts', + }) + ).toBeInTheDocument() + expect( + screen.queryByText('Additional actuary contact') + ).toBeInTheDocument() + expect( + screen.getByRole('link', { + name: 'additionalactuarycontact1@test.com', + }) + ).toBeInTheDocument() + expect( + screen.queryByText('Actuaries’ communication preference') + ).toBeInTheDocument() + expect( + screen.queryByText( + '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() + }) + + it('can render only state contacts for contract only submission', () => { + const stateSubmission = mockContractPackageSubmitted() + stateSubmission.packageSubmissions[0].contractRevision.formData = { + ...stateSubmission.packageSubmissions[0].contractRevision.formData, + submissionType: 'CONTRACT_ONLY', + } + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'State contacts', + }) + ).toBeInTheDocument() + + expect(screen.queryByText('Actuary contacts')).not.toBeInTheDocument() + }) + + it('renders submitted package without errors', () => { + renderWithProviders( + + ) + + // We should never display missing field text on submission summary for submitted packages + expect( + screen.queryByText(/You must provide this information/) + ).toBeNull() + }) + + it('does not include additional actuary contacts heading when this optional field is not provided', () => { + const draftSubmission = mockContractPackageDraft() + if ( + draftSubmission.draftRates && + draftSubmission.draftRates[0].draftRevision + ) { + draftSubmission.draftRates[0].draftRevision.formData = { + ...draftSubmission.draftRates[0].draftRevision.formData, + addtlActuaryContacts: [], + } + renderWithProviders( + + ) + } + expect(screen.queryByText(/Additional actuary contacts/)).toBeNull() + }) +}) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx new file mode 100644 index 0000000000..14503da250 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx @@ -0,0 +1,145 @@ +import { Grid, GridContainer } from '@trussworks/react-uswds' +import styles from '../../../../../components/SubmissionSummarySection/SubmissionSummarySection.module.scss' + +import { SectionHeader } from '../../../../../components/SectionHeader' +import { + ActuaryFirmsRecord, + ActuaryCommunicationRecord, +} from '../../../../../constants/healthPlanPackages' +import { ActuaryContact } from '../../../../../common-code/healthPlanFormDataType' +import { + DataDetail, + DataDetailContactField, +} from '../../../../../components/DataDetail' +import { SectionCard } from '../../../../../components/SectionCard' +import { Contract } from '../../../../../gen/gqlClient' + +export type ContactsSummarySectionProps = { + contract: Contract + editNavigateTo?: string +} + +export const getActuaryFirm = (actuaryContact: ActuaryContact): string => { + if ( + actuaryContact.actuarialFirmOther && + actuaryContact.actuarialFirm === 'OTHER' + ) { + return actuaryContact.actuarialFirmOther + } else if ( + actuaryContact.actuarialFirm && + ActuaryFirmsRecord[actuaryContact.actuarialFirm] + ) { + return ActuaryFirmsRecord[actuaryContact.actuarialFirm] + } else { + return '' + } +} + +export const ContactsSummarySection = ({ + contract, + editNavigateTo, +}: ContactsSummarySectionProps): React.ReactElement => { + const isSubmitted = contract.status === 'SUBMITTED' + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + const rateRev = contract.draftRates + ? contract.draftRates[0].draftRevision + : contract.packageSubmissions[0].rateRevisions[0] + return ( + + + + + +
+ {contractFormData.stateContacts.length > 0 ? ( + contractFormData.stateContacts.map( + (stateContact, index) => ( + + } + /> + ) + ) + ) : ( + + )} +
+
+
+ + {contractFormData.submissionType === 'CONTRACT_AND_RATES' && ( + <> + {rateRev?.formData?.addtlActuaryContacts !== undefined && + rateRev.formData.addtlActuaryContacts.length > 0 && ( +
+ + + + {rateRev.formData?.addtlActuaryContacts.map( + (actuaryContact, index) => ( + + + } + /> + + ) + )} + + +
+ )} +
+ + + +
+ + )} +
+ ) +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.stories.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.stories.tsx new file mode 100644 index 0000000000..e6004c1ae5 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.stories.tsx @@ -0,0 +1,43 @@ +import { Story } from '@storybook/react' +import ProvidersDecorator from '../../../../../../.storybook/providersDecorator' +import { + ContractDetailsSummarySectionV2Props, + ContractDetailsSummarySectionV2 as ContractDetailsSummarySection, +} from './ContractDetailsSummarySectionV2' +import { mockContractPackageDraft } from '../../../../../testHelpers/apolloMocks' + +export default { + title: 'Components/SubmissionSummary/ContractDetailsSummarySection/V2', + component: ContractDetailsSummarySection, + parameters: { + componentSubtitle: + 'ContractDetailsSummarySection displays the Contract Details data for a Draft or State Submission', + }, +} + +const Template: Story = (args) => ( + +) + +export const WithAction = Template.bind({}) +WithAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithAction.args = { + contract: mockContractPackageDraft(), + documentDateLookupTable: { + fakesha: 'Fri Mar 25 2022 16:13:20 GMT-0500 (Central Daylight Time)', + previousSubmissionDate: '01/01/01' + }, + editNavigateTo: 'contract-details', +} + +export const WithoutAction = Template.bind({}) +WithoutAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithoutAction.args = { + contract: mockContractPackageDraft(), + documentDateLookupTable: { + fakesha: 'Fri Mar 25 2022 16:13:20 GMT-0500 (Central Daylight Time)', + previousSubmissionDate: '01/01/01' + }, +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx new file mode 100644 index 0000000000..ff8f47d622 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx @@ -0,0 +1,977 @@ +import { screen, waitFor, within } from '@testing-library/react' +import { renderWithProviders } from '../../../../../testHelpers/jestHelpers' +import { ContractDetailsSummarySectionV2 as ContractDetailsSummarySection } from './ContractDetailsSummarySectionV2' +import { + fetchCurrentUserMock, + mockContractPackageDraft, + mockContractPackageSubmitted, +} from '../../../../../testHelpers/apolloMocks' +import { testS3Client } from '../../../../../testHelpers/s3Helpers' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../../../constants/statutoryRegulatoryAttestation' + +describe('ContractDetailsSummarySection', () => { + const defaultApolloMocks = { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + } + + it('can render draft submission without errors (review and submit behavior)', async () => { + const testContract = { + ...mockContractPackageDraft(), + documents: [ + { + s3URL: 's3://bucketname/key/test1', + name: 'supporting docs test 1', + sha256: 'fakesha', + }, + { + s3URL: 's3://bucketname/key/test3', + name: 'supporting docs test 3', + sha256: 'fakesha', + }, + ], + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + + expect( + await screen.findByRole('heading', { + level: 2, + name: 'Contract details', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('link', { name: 'Edit Contract details' }) + ).toHaveAttribute('href', '/contract-details') + expect( + screen.getByRole('link', { + name: /Edit Contract supporting documents/, + }) + ).toHaveAttribute('href', '/documents') + expect( + screen.queryByRole('link', { + name: 'Download all contract documents', + }) + ).toBeNull() + }) + + it('can render state submission on summary page without errors (submission summary behavior)', async () => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'Contract details', + }) + ).toBeInTheDocument() + expect(screen.queryByText('Edit')).not.toBeInTheDocument() + + //expects loading button on component load + expect(screen.getByText('Loading')).toBeInTheDocument() + + // expects download all button after loading has completed + await waitFor(() => { + expect( + screen.getByRole('link', { + name: 'Download all contract documents', + }) + ).toBeInTheDocument() + }) + }) + + it('can render all contract details fields', async () => { + const contract = mockContractPackageDraft() + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + featureFlags: { '438-attestation': true }, + } + ) + + await waitFor(() => { + expect( + screen.getByRole('definition', { + name: StatutoryRegulatoryAttestationQuestion, + }) + ).toBeInTheDocument() + }) + + expect( + screen.getByRole('definition', { name: 'Contract status' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Contract amendment effective dates', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Managed care entities' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Active federal operating authority', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'This contract action includes new or modified provisions related to the following', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'This contract action does NOT include new or modified provisions related to the following', + }) + ).toBeInTheDocument() + }) + + it('displays correct contract 438 attestation yes and no text and description', async () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + statutoryRegulatoryAttestation: false, + statutoryRegulatoryAttestationDescription: 'No compliance', + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + featureFlags: { '438-attestation': true }, + } + ) + } + await waitFor(() => { + expect( + screen.getByRole('definition', { + name: StatutoryRegulatoryAttestationQuestion, + }) + ).toBeInTheDocument() + }) + + expect( + screen.getByRole('definition', { + name: 'Non-compliance description', + }) + ).toBeInTheDocument() + expect( + await screen.findByText(StatutoryRegulatoryAttestation.NO) + ).toBeInTheDocument() + expect(await screen.findByText('No compliance')).toBeInTheDocument() + }) + + it('displays correct effective dates text for base contract', async () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + contractType: 'BASE', + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + expect(screen.getByText('Contract effective dates')).toBeInTheDocument() + }) + + it('displays correct effective dates text for contract amendment', () => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + expect( + screen.getByText('Contract amendment effective dates') + ).toBeInTheDocument() + }) + + it('render supporting contract docs when they exist', async () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + contractDocuments: [ + { + s3URL: 's3://foo/bar/contract', + name: 'contract test 1', + sha256: 'fakesha', + }, + ], + supportingDocuments: [ + { + s3URL: 's3://bucketname/key/test1', + name: 'supporting docs test 1', + sha256: 'fakesha', + }, + { + s3URL: 's3://bucketname/key/test2', + name: 'supporting docs test 2', + sha256: 'fakesha', + }, + { + s3URL: 's3://bucketname/key/test3', + name: 'supporting docs test 3', + sha256: 'fakesha', + }, + ], + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + await waitFor(() => { + const contractDocsTable = screen.getByRole('table', { + name: 'Contract', + }) + + const supportingDocsTable = screen.getByRole('table', { + name: /Contract supporting documents/, + }) + + expect(contractDocsTable).toBeInTheDocument() + + expect(supportingDocsTable).toBeInTheDocument() + + // check row content + expect( + within(contractDocsTable).getByRole('row', { + name: /contract test 1/, + }) + ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 1') + ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 2') + ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 3') + ).toBeInTheDocument() + + // check correct category on supporting docs + expect( + within(supportingDocsTable).getAllByText('Contract-supporting') + ).toHaveLength(3) + }) + }) + + it('does not render supporting contract documents table when no documents exist', () => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + + expect( + screen.queryByRole('table', { + name: /Contract supporting documents/, + }) + ).toBeNull() + }) + + it('does not render download all button when on previous submission', () => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + expect( + screen.queryByRole('button', { + name: 'Download all contract documents', + }) + ).toBeNull() + }) + + it('renders federal authorities for a medicaid contract', async () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + // Add all medicaid federal authorities, as if medicaid contract being unlocked + federalAuthorities: [ + 'STATE_PLAN', + 'WAIVER_1915B', + 'WAIVER_1115', + 'VOLUNTARY', + 'BENCHMARK', + 'TITLE_XXI', + ], + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + expect( + await screen.findByText( + 'Title XXI Separate CHIP State Plan Authority' + ) + ).toBeInTheDocument() + expect( + await screen.findByText('1115 Waiver Authority') + ).toBeInTheDocument() + expect( + await screen.findByText('1932(a) State Plan Authority') + ).toBeInTheDocument() + expect( + await screen.findByText('1937 Benchmark Authority') + ).toBeInTheDocument() + }) + + it('renders federal authorities for a CHIP contract as expected, removing invalid authorities', async () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + populationCovered: 'CHIP', + // Add all medicaid federal authorities, as if medicaid contract being unlocked + federalAuthorities: [ + 'STATE_PLAN', + 'WAIVER_1915B', + 'WAIVER_1115', + 'VOLUNTARY', + 'BENCHMARK', + 'TITLE_XXI', + ], + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + expect( + await screen.findByText( + 'Title XXI Separate CHIP State Plan Authority' + ) + ).toBeInTheDocument() + expect( + await screen.findByText('1115 Waiver Authority') + ).toBeInTheDocument() + expect( + screen.queryByText('1932(a) State Plan Authority') + ).not.toBeInTheDocument() + expect( + screen.queryByText('1937 Benchmark Authority') + ).not.toBeInTheDocument() + }) + + it('renders inline error when bulk URL is unavailable', async () => { + const s3Provider = { + ...testS3Client(), + getBulkDlURL: async ( + _keys: string[], + _fileName: string + ): Promise => { + return new Error('Error: getBulkDlURL encountered an error') + }, + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + s3Provider, + } + ) + + await waitFor(() => { + expect( + screen.getByText('Contract document download is unavailable') + ).toBeInTheDocument() + }) + }) + + describe('contract provisions', () => { + it('renders provisions and MLR references for a medicaid amendment', () => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + + expect( + screen.getByText('Benefits provided by the managed care plans') + ).toBeInTheDocument() + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).getByText( + 'Benefits provided by the managed care plans' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Pass-through payments in accordance with 42 CFR § 438.6(d)' + ) + ).toBeInTheDocument() + + expect( + within(modifiedProvisions).getByText(/Risk-sharing strategy/) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'State directed payments in accordance with 42 CFR § 438.6(c)' + ) + ).toBeInTheDocument() + + expect( + within(modifiedProvisions).getByText( + 'Medical loss ratio standards in accordance with 42 CFR § 438.8' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Network adequacy standards' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Enrollment/disenrollment process' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + /Non-risk payment arrangements/ + ) + ).toBeInTheDocument() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).getByText( + 'Geographic areas served by the managed care plans' + ) + ).toBeInTheDocument() + expect( + within(unmodifiedProvisions).getByText( + 'Payments to MCOs and PIHPs for enrollees that are a patient in an institution for mental disease in accordance with 42 CFR § 438.6(e)' + ) + ).toBeInTheDocument() + expect( + within(unmodifiedProvisions).getByText( + 'Incentive arrangements in accordance with 42 CFR § 438.6(b)(2)' + ) + ).toBeInTheDocument() + expect( + within(unmodifiedProvisions).getByText( + 'Grievance and appeal system' + ) + ).toBeInTheDocument() + + expect( + within(unmodifiedProvisions).getByText( + 'Length of the contract period' + ) + ).toBeInTheDocument() + + expect( + within(unmodifiedProvisions).getByText( + 'Other financial, payment, incentive or related contractual provisions' + ) + ).toBeInTheDocument() + }) + + it('renders provisions and MLR references for a medicaid base contract', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + contractType: 'BASE', + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes provisions related to the following' + ) + expect( + within(modifiedProvisions).getByText( + 'In Lieu-of Services and Settings (ILOSs) in accordance with 42 CFR § 438.3(e)(2)' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Pass-through payments in accordance with 42 CFR § 438.6(d)' + ) + ).toBeInTheDocument() + + expect( + within(modifiedProvisions).getByText(/Risk-sharing strategy/) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'State directed payments in accordance with 42 CFR § 438.6(c)' + ) + ).toBeInTheDocument() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include provisions related to the following' + ) + expect( + within(unmodifiedProvisions).getByText( + 'Payments to MCOs and PIHPs for enrollees that are a patient in an institution for mental disease in accordance with 42 CFR § 438.6(e)' + ) + ).toBeInTheDocument() + expect( + within(unmodifiedProvisions).getByText( + 'Incentive arrangements in accordance with 42 CFR § 438.6(b)(2)' + ) + ).toBeInTheDocument() + }) + + it('renders provisions with correct MLR references for CHIP amendment', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + populationCovered: 'CHIP', + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + expect( + screen.getByText('Benefits provided by the managed care plans') + ).toBeInTheDocument() + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).getByText( + 'Benefits provided by the managed care plans' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Network adequacy standards 42 CFR § 457.1218' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Enrollment/disenrollment process 42 CFR § 457.1210 and 457.1212' + ) + ).toBeInTheDocument() + expect( + within(modifiedProvisions).getByText( + 'Non-risk payment arrangements 42 CFR 457.10 and 457.1201(c)' + ) + ).toBeInTheDocument() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).getByText( + 'Grievance and appeal system 42 CFR § 457.1260' + ) + ).toBeInTheDocument() + + expect( + within(unmodifiedProvisions).getByText( + 'Grievance and appeal system 42 CFR § 457.1260' + ) + ).toBeInTheDocument() + + expect( + within(unmodifiedProvisions).getByText( + 'Length of the contract period' + ) + ).toBeInTheDocument() + + // not a CHIP provision, even if saved from an unlock, it should not show up on summary page once population is CHIP + expect( + within(unmodifiedProvisions).queryByText( + 'Other financial, payment, incentive or related contractual provisions' + ) + ).toBeNull() + }) + + it('shows missing field error when provisions list is empty and section is in edit mode', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + inLieuServicesAndSettings: undefined, + modifiedBenefitsProvided: undefined, + modifiedGeoAreaServed: undefined, + modifiedMedicaidBeneficiaries: undefined, + modifiedRiskSharingStrategy: undefined, + modifiedIncentiveArrangements: undefined, + modifiedWitholdAgreements: undefined, + modifiedStateDirectedPayments: undefined, + modifiedPassThroughPayments: undefined, + modifiedPaymentsForMentalDiseaseInstitutions: undefined, + modifiedMedicalLossRatioStandards: undefined, + modifiedOtherFinancialPaymentIncentive: undefined, + modifiedEnrollmentProcess: undefined, + modifiedGrevienceAndAppeal: undefined, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, + statutoryRegulatoryAttestation: undefined, + statutoryRegulatoryAttestationDescription: undefined, + } + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeInTheDocument() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeInTheDocument() + }) + + it('shows missing field error when provisions list is incomplete and summary section is in edit mode', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + modifiedBenefitsProvided: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: null, + modifiedNonRiskPaymentArrangements: false, + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeInTheDocument() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeInTheDocument() + }) + + it('does not show missing field error when provisions list is incomplete and summary section is in view only mode', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + modifiedBenefitsProvided: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: true, + modifiedNonRiskPaymentArrangements: false, + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + }) + + it('does not show missing field error for CHIP amendment when all provisions required are valid', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + modifiedBenefitsProvided: false, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: true, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: true, + modifiedNonRiskPaymentArrangements: false, + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + }) + + it('does not show missing field error for Medicaid amendment when all provisions required are valid', () => { + const contract = mockContractPackageDraft() + if (contract.draftRevision) { + contract.draftRevision.formData = { + ...contract.draftRevision.formData, + inLieuServicesAndSettings: true, + modifiedBenefitsProvided: false, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: false, + modifiedRiskSharingStrategy: false, + modifiedIncentiveArrangements: false, + modifiedWitholdAgreements: false, + modifiedStateDirectedPayments: false, + modifiedPassThroughPayments: false, + modifiedPaymentsForMentalDiseaseInstitutions: false, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: false, + modifiedNonRiskPaymentArrangements: false, + } + + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + } + + const modifiedProvisions = screen.getByLabelText( + 'This contract action includes new or modified provisions related to the following' + ) + expect( + within(modifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + + const unmodifiedProvisions = screen.getByLabelText( + 'This contract action does NOT include new or modified provisions related to the following' + ) + expect( + within(unmodifiedProvisions).queryByText( + /You must provide this information/ + ) + ).toBeNull() + }) + }) +}) 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 new file mode 100644 index 0000000000..55c195ad6c --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx @@ -0,0 +1,336 @@ +import React, { useState } from 'react' +import { DataDetail } from '../../../../../components/DataDetail' +import { SectionHeader } from '../../../../../components/SectionHeader' +import { UploadedDocumentsTable } from '../../../../../components/SubmissionSummarySection' +import { + ContractExecutionStatusRecord, + FederalAuthorityRecord, + ManagedCareEntityRecord, +} from '../../../../../constants/index' +import { useS3 } from '../../../../../contexts/S3Context' +import { formatCalendarDate } from '../../../../../common-code/dateHelpers' +import { DoubleColumnGrid } from '../../../../../components/DoubleColumnGrid' +import { DownloadButton } from '../../../../../components/DownloadButton' +import { usePreviousSubmission } from '../../../../../hooks/usePreviousSubmission' +import styles from '../../../../../components/SubmissionSummarySection/SubmissionSummarySection.module.scss' + +import { + sortModifiedProvisions, + isMissingProvisions, + getProvisionDictionary, +} from '../../../../../common-code/ContractTypeProvisions' +import { DataDetailCheckboxList } from '../../../../../components/DataDetail/DataDetailCheckboxList' +import { + isBaseContract, + isCHIPOnly, + isContractWithProvisions, + isSubmitted, +} from '../../../../../common-code/ContractType' +import { + federalAuthorityKeysForCHIP, + CHIPFederalAuthority, +} from '../../../../../common-code/healthPlanFormDataType' +import { DocumentDateLookupTableType } from '../../../../../documentHelpers/makeDocumentDateLookupTable' +import { recordJSException } from '../../../../../otelHelpers' +import useDeepCompareEffect from 'use-deep-compare-effect' +import { InlineDocumentWarning } from '../../../../../components/DocumentWarning' +import { useLDClient } from 'launchdarkly-react-client-sdk' +import { featureFlags } from '../../../../../common-code/featureFlags' +import { Grid } from '@trussworks/react-uswds' +import { booleanAsYesNoFormValue } from '../../../../../components/Form/FieldYesNo' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../../../constants/statutoryRegulatoryAttestation' +import { SectionCard } from '../../../../../components/SectionCard' +import { Contract } from '../../../../../gen/gqlClient' + +export type ContractDetailsSummarySectionV2Props = { + contract: Contract + editNavigateTo?: string + documentDateLookupTable: DocumentDateLookupTableType + isCMSUser?: boolean + submissionName: string + onDocumentError?: (error: true) => void +} + +function renderDownloadButton(zippedFilesURL: string | undefined | Error) { + if (zippedFilesURL instanceof Error) { + return ( + + ) + } + return ( + + ) +} + +export const ContractDetailsSummarySectionV2 = ({ + contract, + editNavigateTo, // this is the edit link for the section. When this prop exists, summary section is loaded in edit mode + documentDateLookupTable, + submissionName, + onDocumentError, +}: ContractDetailsSummarySectionV2Props): React.ReactElement => { + // Checks if submission is a previous submission + const isPreviousSubmission = usePreviousSubmission() + // Get the zip file for the contract + const { getKey, getBulkDlURL } = useS3() + const [zippedFilesURL, setZippedFilesURL] = useState< + string | undefined | Error + >(undefined) + const ldClient = useLDClient() + + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + const contract438Attestation = ldClient?.variation( + featureFlags.CONTRACT_438_ATTESTATION.flag, + featureFlags.CONTRACT_438_ATTESTATION.defaultValue + ) + + const attestationYesNo = + contractFormData.statutoryRegulatoryAttestation != null && + booleanAsYesNoFormValue(contractFormData.statutoryRegulatoryAttestation) + + const contractSupportingDocuments = contractFormData?.supportingDocuments + const isEditing = !isSubmitted(contract) && editNavigateTo !== undefined + const applicableFederalAuthorities = isCHIPOnly(contract) + ? contractFormData.federalAuthorities.filter((authority) => + federalAuthorityKeysForCHIP.includes( + authority as CHIPFederalAuthority + ) + ) + : contractFormData?.federalAuthorities + const [modifiedProvisions, unmodifiedProvisions] = + sortModifiedProvisions(contract) + const provisionsAreInvalid = isMissingProvisions(contract) && isEditing + + useDeepCompareEffect(() => { + // skip getting urls of this if this is a previous contract or draft + if (!isSubmitted(contract) || isPreviousSubmission) return + + // get all the keys for the documents we want to zip + async function fetchZipUrl() { + const keysFromDocs = contractFormData.contractDocuments + .concat(contractSupportingDocuments) + .map((doc) => { + const key = getKey(doc.s3URL) + if (!key) return '' + return key + }) + .filter((key) => key !== '') + + // call the lambda to zip the files and get the url + const zippedURL = await getBulkDlURL( + keysFromDocs, + submissionName + '-contract-details.zip', + 'HEALTH_PLAN_DOCS' + ) + if (zippedURL instanceof Error) { + const msg = `ERROR: getBulkDlURL failed to generate contract document URL. ID: ${contract.id} Message: ${zippedURL}` + console.info(msg) + + if (onDocumentError) { + onDocumentError(true) + } + + recordJSException(msg) + } + + setZippedFilesURL(zippedURL) + } + + void fetchZipUrl() + }, [ + getKey, + getBulkDlURL, + contract, + contractSupportingDocuments, + submissionName, + isPreviousSubmission, + ]) + + return ( + + + {isSubmitted(contract) && + !isPreviousSubmission && + renderDownloadButton(zippedFilesURL)} + +
+ {contract438Attestation && ( + + + {attestationYesNo !== false && + attestationYesNo !== undefined && ( + + )} + + {attestationYesNo === 'NO' && ( + + + + )} + + )} + + + + + ) + } + /> + + ) + } + /> + + {isContractWithProvisions(contract) && ( + + + {provisionsAreInvalid ? null : ( + + )} + + + + {provisionsAreInvalid ? null : ( + + )} + + + )} +
+ {contractFormData.contractDocuments && ( + + )} + {contractSupportingDocuments && ( + + )} +
+ ) +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.stories.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.stories.tsx new file mode 100644 index 0000000000..7bfa93a01f --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.stories.tsx @@ -0,0 +1,46 @@ +import { Story } from '@storybook/react' +import ProvidersDecorator from '../../../../../../.storybook/providersDecorator' +import { + RateDetailsSummarySectionV2Props, + RateDetailsSummarySectionV2, +} from './RateDetailsSummarySectionV2' +import { mockContractPackageDraft } from '../../../../../testHelpers/apolloMocks' + +export default { + title: 'Components/SubmissionSummary/RateDetailsSummarySection/V2', + component: RateDetailsSummarySectionV2, + parameters: { + componentSubtitle: + 'RateDetailsSummarySection displays the Rate Details data for a Draft or State Submission', + }, +} + +const Template: Story = (args) => ( + +) + +export const WithAction = Template.bind({}) +WithAction.decorators = [(Story) => ProvidersDecorator(Story, {})] +const contract = mockContractPackageDraft() + +WithAction.args = { + contract: contract, + editNavigateTo: 'contract-details', + submissionName: 'StoryBook', + statePrograms: [], + documentDateLookupTable: { + fakesha: 'Fri Mar 25 2022 16:13:20 GMT-0500 (Central Daylight Time)', + previousSubmissionDate: '01/01/01' + }, +} + +export const WithoutAction = Template.bind({}) +WithoutAction.decorators = [(Story) => ProvidersDecorator(Story, {})] +WithoutAction.args = { + contract: contract, + submissionName: 'StoryBook', + documentDateLookupTable: { + fakesha: 'Fri Mar 25 2022 16:13:20 GMT-0500 (Central Daylight Time)', + previousSubmissionDate: '01/01/01' + }, +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx new file mode 100644 index 0000000000..e3909417b7 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx @@ -0,0 +1,786 @@ +import { screen, waitFor, within } from '@testing-library/react' +import { + mockContractPackageDraft, + mockContractPackageSubmitted, + mockMNState, + fetchCurrentUserMock, + mockValidCMSUser, +} from '../../../../../testHelpers/apolloMocks' +import { renderWithProviders } from '../../../../../testHelpers/jestHelpers' +import { RateDetailsSummarySectionV2 as RateDetailsSummarySection } from './RateDetailsSummarySectionV2' +import { Rate } from '../../../../../gen/gqlClient' +import { testS3Client } from '../../../../../testHelpers/s3Helpers' + +describe('RateDetailsSummarySection', () => { + const draftContract = mockContractPackageDraft() + const submittedContract = mockContractPackageSubmitted() + const statePrograms = mockMNState().programs + const makeMockRateInfos = (): Rate[] => { + return [ + { + id: '1234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + status: 'DRAFT', + state: mockMNState(), + stateCode: 'MN', + stateNumber: 5, + revisions: [], + draftRevision: { + id: '1234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + contractRevisions: [], + formData: { + rateType: 'NEW', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://foo/bar/rate', + name: 'rate docs test 1', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + rateDateStart: new Date('01/01/2021'), + rateDateEnd: new Date('12/31/2021'), + rateDateCertified: new Date('12/31/2020'), + amendmentEffectiveDateStart: new Date('01/01/2021'), + amendmentEffectiveDateEnd: new Date('12/31/2021'), + rateProgramIDs: [ + 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', + ], + certifyingActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Jimmy Jimerson', + titleRole: 'Certifying Actuary', + email: 'jj.actuary@test.com', + }, + ], + addtlActuaryContacts: [], + packagesWithSharedRateCerts: [], + }, + }, + }, + { + id: '5678', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + status: 'DRAFT', + state: mockMNState(), + stateCode: 'MN', + stateNumber: 5, + revisions: [], + draftRevision: { + id: '1234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + contractRevisions: [], + formData: { + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://foo/bar/rate', + name: 'rate docs test 2', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + rateDateStart: new Date('01/01/2021'), + rateDateEnd: new Date('12/31/2021'), + rateDateCertified: new Date('12/31/2020'), + amendmentEffectiveDateStart: new Date('01/01/2021'), + amendmentEffectiveDateEnd: new Date('12/31/2021'), + rateProgramIDs: [ + 'd95394e5-44d1-45df-8151-1cc1ee66f100', + ], + certifyingActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Timmy Timerson', + titleRole: 'Certifying Actuary', + email: 'tt.actuary@test.com', + }, + ], + addtlActuaryContacts: [], + packagesWithSharedRateCerts: [], + }, + }, + }, + ] + } + + const apolloProvider = { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + ], + } + + afterEach(() => jest.clearAllMocks()) + + it('can render draft contract with rates without errors', () => { + renderWithProviders( + , + { + apolloProvider, + } + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'Rate details', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('link', { name: 'Edit Rate details' }) + ).toHaveAttribute('href', '/rate-details') + }) + + it('can render submitted contract without errors', async () => { + renderWithProviders( + , + { + apolloProvider, + } + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'Rate details', + }) + ).toBeInTheDocument() + // Is this the best way to check that the link is not present? + expect(screen.queryByText('Edit')).not.toBeInTheDocument() + + //expects loading button on component load + expect(screen.getByText('Loading')).toBeInTheDocument() + + // expects download all button after loading has completed + await waitFor(() => { + expect( + screen.getByRole('link', { + name: 'Download all rate documents', + }) + ).toBeInTheDocument() + }) + }) + + it('can render all rate details fields for amendment to prior rate certification submission', () => { + renderWithProviders( + , + { + apolloProvider, + } + ) + expect( + screen.getByRole('definition', { name: 'Rate certification type' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Does the actuary certify capitation rates specific to each rate cell or a rate range?', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Rating period of original rate certification', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Date certified for rate amendment', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: 'Rate amendment effective dates', + }) + ).toBeInTheDocument() + }) + + it('can render correct rate name for new rate submission', async () => { + const contract = mockContractPackageSubmitted() + contract.packageSubmissions[0].rateRevisions[0].formData.rateCertificationName = + 'MCR-MN-0005-SNBC-RATE-20221013-20221013-CERTIFICATION-20221013' + + const statePrograms = mockMNState().programs + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider, + } + ) + }) + const rateName = + 'MCR-MN-0005-SNBC-RATE-20221013-20221013-CERTIFICATION-20221013' + expect(screen.getByText(rateName)).toBeInTheDocument() + }) + + it('can render correct rate name for AMENDMENT rate submission', () => { + const submission = { + ...mockContractPackageDraft(), + rateDateStart: new Date('2022-01-25'), + rateDateEnd: new Date('2023-01-25'), + rateDateCertified: new Date('2022-01-26'), + amendmentEffectiveDateStart: new Date('2022-02-25'), + amendmentEffectiveDateEnd: new Date('2023-02-26'), + } + const draftRate = submission?.draftRates + if (draftRate) { + const draftRev = draftRate[0].draftRevision + if (draftRev) { + draftRev.formData.rateCertificationName = + 'MCR-MN-0005-SNBC-RATE-20221013-20221013-CERTIFICATION-20221013' + + const statePrograms = mockMNState().programs + + renderWithProviders( + , + { + apolloProvider, + } + ) + } + } + const rateName = + 'MCR-MN-0005-SNBC-RATE-20221013-20221013-CERTIFICATION-20221013' + expect(screen.getByText(rateName)).toBeInTheDocument() + }) + + it('can render all rate details fields for new rate certification submission', async () => { + const statePrograms = mockMNState().programs + const contract = mockContractPackageSubmitted() + contract.packageSubmissions[0].rateRevisions[0].formData.rateCertificationName = + 'MCR-MN-0005-SNBC-RATE-20221014-20221014-CERTIFICATION-20221014' + contract.packageSubmissions[0].rateRevisions[0].formData.rateType = + 'NEW' + contract.packageSubmissions[0].rateRevisions[0].formData.amendmentEffectiveDateStart = + null + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider, + } + ) + }) + + const rateName = + 'MCR-MN-0005-SNBC-RATE-20221014-20221014-CERTIFICATION-20221014' + + expect(screen.getByText(rateName)).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Rate certification type' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Rating period' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Date certified' }) + ).toBeInTheDocument() + }) + + it('renders supporting rates docs when they exist', async () => { + const draftContract = mockContractPackageDraft() + if ( + draftContract.draftRates && + draftContract.draftRates[0].draftRevision + ) { + const contractWithRateDocsFormData = { + ...draftContract.draftRates[0].draftRevision.formData, + rateDocuments: [ + { + s3URL: 's3://foo/bar/rate', + name: 'rate docs test 1', + sha256: 'fakesha', + }, + ], + supportingDocuments: [ + { + s3URL: 's3://foo/bar/test-2', + name: 'supporting docs test 2', + sha256: 'fakesha', + }, + { + s3URL: 's3://foo/bar/test-3', + name: 'supporting docs test 3', + sha256: 'fakesha', + }, + ], + } + const contractWithRateDocs = { + ...draftContract, + } + if ( + contractWithRateDocs.draftRates && + contractWithRateDocs.draftRates[0].draftRevision + ) { + contractWithRateDocs.draftRates[0].draftRevision.formData = + contractWithRateDocsFormData + + renderWithProviders( + , + { + apolloProvider, + } + ) + } + } + await waitFor(() => { + const supportingDocsTable = screen.getByRole('table', { + name: /Rate supporting documents/, + }) + const rateDocsTable = screen.getByRole('table', { + name: /Rate certification/, + }) + + expect(rateDocsTable).toBeInTheDocument() + expect(supportingDocsTable).toBeInTheDocument() + + const supportingDocsTableRows = + within(supportingDocsTable).getAllByRole('rowgroup') + expect(supportingDocsTableRows).toHaveLength(2) + + // check row content + expect( + within(rateDocsTable).getByRole('row', { + name: /rate docs test 1/, + }) + ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 2') + ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 3') + ).toBeInTheDocument() + + // check correct category on supporting docs + expect( + within(supportingDocsTable).getAllByText('Rate-supporting') + ).toHaveLength(2) + }) + }) + + it('does not render supporting rate documents when they do not exist', () => { + const draftContract = mockContractPackageSubmitted() + + if ( + draftContract.draftRates && + draftContract.draftRates[0].draftRevision + ) { + const contractWithRateDocsFormData = { + ...draftContract.draftRates[0].draftRevision.formData, + rateDocuments: [ + { + s3URL: 's3://foo/bar/rate', + name: 'rate docs test 1', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + } + const contractWithRateDocs = { + ...draftContract, + } + if ( + contractWithRateDocs.draftRates && + contractWithRateDocs.draftRates[0].draftRevision + ) { + contractWithRateDocs.draftRates[0].draftRevision.formData = + contractWithRateDocsFormData + + renderWithProviders( + , + { + apolloProvider, + } + ) + } + } + expect( + screen.queryByRole('table', { + name: /Rate supporting documents/, + }) + ).toBeNull() + }) + + it('does not render download all button when on previous submission', async () => { + await waitFor(() => + renderWithProviders( + , + { + apolloProvider, + } + ) + ) + + expect( + screen.queryByRole('button', { + name: 'Download all rate documents', + }) + ).toBeNull() + }) + + it('renders rate cell capitation type', () => { + renderWithProviders( + , + { + apolloProvider, + } + ) + expect( + screen.getByRole('definition', { + name: 'Does the actuary certify capitation rates specific to each rate cell or a rate range?', + }) + ).toBeInTheDocument() + expect( + screen.getByText( + 'Certification of capitation rates specific to each rate cell' + ) + ).toBeInTheDocument() + }) + + it('renders rate range capitation type', () => { + const draftContract = mockContractPackageDraft() + if ( + draftContract.draftRates && + draftContract.draftRates[0].draftRevision + ) { + draftContract.draftRates[0].draftRevision.formData.rateCapitationType = + 'RATE_RANGE' + renderWithProviders( + , + { + apolloProvider, + } + ) + } + expect( + screen.getByRole('definition', { + name: 'Does the actuary certify capitation rates specific to each rate cell or a rate range?', + }) + ).toBeInTheDocument() + expect( + screen.getByText( + 'Certification of rate ranges of capitation rates per rate cell' + ) + ).toBeInTheDocument() + }) + + it('renders programs that apply to rate certification', async () => { + const draftContract = mockContractPackageDraft() + if ( + draftContract.draftRates && + draftContract.draftRates[0].draftRevision + ) { + draftContract.draftRates[0].draftRevision.formData.rateProgramIDs = + [ + 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', + 'd95394e5-44d1-45df-8151-1cc1ee66f100', + ] + renderWithProviders( + , + { + apolloProvider, + } + ) + } + const programElement = screen.getByRole('definition', { + name: 'Programs this rate certification covers', + }) + expect(programElement).toBeInTheDocument() + const programList = within(programElement).getByText('SNBC, PMAP') + expect(programList).toBeInTheDocument() + }) + + it('renders rate program names even when rate program ids are missing', async () => { + const draftContract = mockContractPackageDraft() + if ( + draftContract.draftRevision && + draftContract.draftRates && + draftContract.draftRates[0].draftRevision + ) { + draftContract.draftRates[0].draftRevision.formData.rateProgramIDs = + [] + draftContract.draftRevision.formData.programIDs = [ + 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', + 'd95394e5-44d1-45df-8151-1cc1ee66f100', + ] + renderWithProviders( + , + { + apolloProvider, + } + ) + } + const programElement = screen.getByRole('definition', { + name: 'Programs this rate certification covers', + }) + expect(programElement).toBeInTheDocument() + const programList = within(programElement).getByText('SNBC, PMAP') + expect(programList).toBeInTheDocument() + }) + + it('renders multiple rate certifications with program names', async () => { + const draftContract = mockContractPackageDraft() + draftContract.draftRates = makeMockRateInfos() + renderWithProviders( + , + { + apolloProvider, + } + ) + const programList = screen.getAllByRole('definition', { + name: 'Programs this rate certification covers', + }) + expect(programList).toHaveLength(2) + expect(programList[0]).toHaveTextContent('SNBC') + expect(programList[1]).toHaveTextContent('PMAP') + }) + + it('renders multiple rate certifications with rate type', async () => { + const draftContract = mockContractPackageDraft() + draftContract.draftRates = makeMockRateInfos() + + renderWithProviders( + , + { + apolloProvider, + } + ) + const certType = screen.getAllByRole('definition', { + name: 'Rate certification type', + }) + expect(certType).toHaveLength(2) + expect(certType[0]).toHaveTextContent('New') + expect(certType[1]).toHaveTextContent('Amendment') + }) + + it('renders multiple rate certifications with documents', async () => { + const draftSubmission = mockContractPackageDraft() + draftSubmission.draftRates = makeMockRateInfos() + renderWithProviders( + , + { + apolloProvider, + } + ) + await waitFor(() => { + const rateDocsTables = screen.getAllByRole('table', { + name: /Rate certification/, + }) + expect(rateDocsTables).toHaveLength(2) + expect( + within(rateDocsTables[0]).getByRole('row', { + name: /rate docs test 1/, + }) + ).toBeInTheDocument() + expect( + within(rateDocsTables[1]).getByRole('row', { + name: /rate docs test 2/, + }) + ).toBeInTheDocument() + }) + }) + + it('renders multiple rate certifications with certifying actuary', async () => { + const draftContract = mockContractPackageDraft() + draftContract.draftRates = makeMockRateInfos() + renderWithProviders( + , + { + apolloProvider, + } + ) + await waitFor(() => { + const certifyingActuary = screen.getAllByRole('definition', { + name: 'Certifying actuary', + }) + expect(certifyingActuary).toHaveLength(2) + expect( + within(certifyingActuary[0]).queryByRole('link', { + name: 'jj.actuary@test.com', + }) + ).toBeInTheDocument() + expect( + within(certifyingActuary[1]).queryByRole('link', { + name: 'tt.actuary@test.com', + }) + ).toBeInTheDocument() + }) + }) + + it('renders submitted package without errors', () => { + renderWithProviders( + , + { + apolloProvider, + } + ) + + expect(screen.queryByRole('link', { name: 'Edit' })).toBeNull() + // We should never display missing field text on submission summary for submitted packages + expect( + screen.queryByText(/You must provide this information/) + ).toBeNull() + }) + + it('renders inline error when bulk URL is unavailable', async () => { + const s3Provider = { + ...testS3Client(), + getBulkDlURL: async ( + _keys: string[], + _fileName: string + ): Promise => { + return new Error('Error: getBulkDlURL encountered an error') + }, + } + renderWithProviders( + , + { + s3Provider, + apolloProvider, + } + ) + + await waitFor(() => { + expect( + screen.getByText('Rate document download is unavailable') + ).toBeInTheDocument() + }) + }) +}) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx new file mode 100644 index 0000000000..2b9e16b167 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx @@ -0,0 +1,320 @@ +import React, { useState } from 'react' +import { DataDetail } from '../../../../../components/DataDetail' +import { SectionHeader } from '../../../../../components/SectionHeader' +import { useS3 } from '../../../../../contexts/S3Context' +import { formatCalendarDate } from '../../../../../common-code/dateHelpers' +import { DoubleColumnGrid } from '../../../../../components/DoubleColumnGrid' +import { DownloadButton } from '../../../../../components/DownloadButton' +import { UploadedDocumentsTable } from '../../../../../components/SubmissionSummarySection' +import { usePreviousSubmission } from '../../../../../hooks/usePreviousSubmission' +import styles from '../../../../../components/SubmissionSummarySection/SubmissionSummarySection.module.scss' + +import { recordJSException } from '../../../../../otelHelpers' +import { DataDetailMissingField } from '../../../../../components/DataDetail/DataDetailMissingField' +import { DataDetailContactField } from '../../../../../components/DataDetail/DataDetailContactField/DataDetailContactField' +import { DocumentDateLookupTableType } from '../../../../../documentHelpers/makeDocumentDateLookupTable' +import useDeepCompareEffect from 'use-deep-compare-effect' +import { InlineDocumentWarning } from '../../../../../components/DocumentWarning' +import { SectionCard } from '../../../../../components/SectionCard' +import { + Rate, + Contract, + Program, + RateRevision, + RateFormData, +} from '../../../../../gen/gqlClient' + +export type RateDetailsSummarySectionV2Props = { + contract: Contract + editNavigateTo?: string + documentDateLookupTable: DocumentDateLookupTableType + isCMSUser?: boolean + submissionName: string + statePrograms: Program[] + onDocumentError?: (error: true) => void +} + +export function renderDownloadButton( + zippedFilesURL: string | undefined | Error +) { + if (zippedFilesURL instanceof Error) { + return ( + + ) + } + return ( + + ) +} + +export const RateDetailsSummarySectionV2 = ({ + contract, + editNavigateTo, + documentDateLookupTable, + submissionName, + statePrograms, + onDocumentError, +}: RateDetailsSummarySectionV2Props): React.ReactElement => { + const isSubmitted = contract.status === 'SUBMITTED' + const isEditing = !isSubmitted && editNavigateTo !== undefined + const isPreviousSubmission = usePreviousSubmission() + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + const rates = + contract.draftRates || contract.packageSubmissions[0].rateRevisions + const { getKey, getBulkDlURL } = useS3() + const [zippedFilesURL, setZippedFilesURL] = useState< + string | undefined | Error + >(undefined) + + const rateCapitationType = (rate: Rate | RateRevision) => { + const rateFormData = getRateFormData(rate) + return rateFormData.rateCapitationType + ? rateFormData.rateCapitationType === 'RATE_CELL' + ? 'Certification of capitation rates specific to each rate cell' + : 'Certification of rate ranges of capitation rates per rate cell' + : '' + } + + const ratePrograms = (rate: Rate | RateRevision) => { + /* if we have rateProgramIDs, use them, otherwise use programIDs */ + let programIDs = [] as string[] + const rateFormData = getRateFormData(rate) + + if ( + rateFormData.rateProgramIDs && + rateFormData.rateProgramIDs.length > 0 + ) { + programIDs = rateFormData.rateProgramIDs + } else if ( + contractFormData?.programIDs && + contractFormData?.programIDs.length > 0 + ) { + programIDs = contractFormData.programIDs + } + return programIDs + ? statePrograms + .filter((p) => programIDs.includes(p.id)) + .map((p) => p.name) + : undefined + } + + const rateCertificationType = (rate: Rate | RateRevision) => { + const rateFormData = getRateFormData(rate) + if (rateFormData.rateType === 'AMENDMENT') { + return 'Amendment to prior rate certification' + } + if (rateFormData.rateType === 'NEW') { + return 'New rate certification' + } + } + + const getRateFormData = (rate: Rate | RateRevision): RateFormData => { + const isRateRev = 'formData' in rate + if (!isRateRev) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return rate.draftRevision!.formData + } else { + return rate.formData + } + } + + useDeepCompareEffect(() => { + // skip getting urls of this if this is a previous submission or draft + if (!isSubmitted || isPreviousSubmission) return + + // get all the keys for the documents we want to zip + async function fetchZipUrl() { + const submittedRates = contract.packageSubmissions[0].rateRevisions + if (submittedRates !== undefined) { + const keysFromDocs = submittedRates + .flatMap((rateInfo) => + rateInfo.formData.rateDocuments.concat( + rateInfo.formData.supportingDocuments + ) + ) + .map((doc) => { + const key = getKey(doc.s3URL) + if (!key) return '' + return key + }) + .filter((key) => key !== '') + + // call the lambda to zip the files and get the url + const zippedURL = await getBulkDlURL( + keysFromDocs, + submissionName + '-rate-details.zip', + 'HEALTH_PLAN_DOCS' + ) + if (zippedURL instanceof Error) { + const msg = `ERROR: getBulkDlURL failed to generate supporting document URL. ID: ${contract.id} Message: ${zippedURL}` + console.info(msg) + + if (onDocumentError) { + onDocumentError(true) + } + + recordJSException(msg) + } + + setZippedFilesURL(zippedURL) + } + } + + void fetchZipUrl() + }, [ + getKey, + getBulkDlURL, + contract, + submissionName, + isSubmitted, + isPreviousSubmission, + ]) + + return ( + + + {isSubmitted && + !isPreviousSubmission && + renderDownloadButton(zippedFilesURL)} + + {rates.length > 0 ? ( + rates.map((rate) => { + const rateFormData = getRateFormData(rate) + return ( + +

+ {rateFormData.rateCertificationName} +

+
+ + {ratePrograms && ( + + )} + + + ) + } + /> + + {rateFormData?.amendmentEffectiveDateStart ? ( + + ) : null} + {rateFormData + .certifyingActuaryContacts[0] && ( + + } + /> + )} + + +
+ {rateFormData?.rateDocuments && ( + + )} + {rateFormData?.supportingDocuments && ( + + )} +
+ ) + }) + ) : ( + + )} +
+ ) +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.test.tsx new file mode 100644 index 0000000000..175a443a23 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.test.tsx @@ -0,0 +1,148 @@ +import { screen, waitFor } from '@testing-library/react' +import { + fetchCurrentUserMock, +} from '../../../../../testHelpers/apolloMocks' +import { renderWithProviders } from '../../../../../testHelpers/jestHelpers' +import { ReviewSubmitV2 } from './ReviewSubmitV2' +import { fetchContractMockSuccess } from '../../../../../testHelpers/apolloMocks' +import { Route, Routes } from 'react-router-dom' +import { RoutesRecord } from '../../../../../constants' + +// Wrap test component in some top level routes to allow getParams to be tested +const wrapInRoutes = (children: React.ReactNode) => { + return ( + + + } + /> + + ) +} + +describe.skip('ReviewSubmit', () => { + it.skip('renders without errors', async () => { + renderWithProviders(wrapInRoutes(), + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ statusCode: 200 }), + fetchContractMockSuccess({ contract: { id: 'test-abc-123' } }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123/edit/review-and-submit', + } + }) + await waitFor(() => { + expect( + screen.getByRole('heading', { name: 'Contract details' }) + ).toBeInTheDocument() + }) + + }) + + it.skip('displays edit buttons for every section', async () => { + renderWithProviders(, { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + }) + + await waitFor(() => { + const sectionHeadings = screen.queryAllByRole('heading', { + level: 2, + }) + const editButtons = screen.queryAllByRole('button', { + name: 'Edit', + }) + expect(sectionHeadings.length).toBeGreaterThanOrEqual( + editButtons.length + ) + }) + }) + + it.skip('does not display zip download buttons', async () => { + renderWithProviders(, { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + }) + + await waitFor(() => { + const bulkDownloadButtons = screen.queryAllByRole('button', { + name: /documents/, + }) + expect(bulkDownloadButtons).toHaveLength(0) + }) + }) + + it.skip('renders info from a DraftSubmission', async () => { + renderWithProviders(, { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + }) + + await waitFor(() => { + expect( + screen.getByRole('heading', { name: 'Contract details' }) + ).toBeInTheDocument() + + expect( + screen.getByRole('heading', { name: 'State contacts' }) + ).toBeInTheDocument() + + const sectionHeadings = screen.queryAllByRole('heading', { + level: 2, + }) + const editButtons = screen.queryAllByRole('button', { + name: 'Edit', + }) + expect(sectionHeadings.length).toBeGreaterThanOrEqual( + editButtons.length + ) + + const submissionDescription = + screen.queryByText('A real submission') + expect(submissionDescription).toBeInTheDocument() + }) + }) + + it.skip('displays back and save as draft buttons', async () => { + renderWithProviders(, { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + }) + + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'Back', + }) + ).toBeDefined() + ) + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'Save as draft', + }) + ).toBeDefined() + ) + }) + + it.skip('displays submit button', async () => { + renderWithProviders(, { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + }) + + await waitFor(() => + expect(screen.getByTestId('form-submit')).toBeDefined() + ) + }) +}) \ No newline at end of file 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 new file mode 100644 index 0000000000..5984d75747 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx @@ -0,0 +1,187 @@ +import { + GridContainer, + ModalRef, + ModalToggleButton, +} from '@trussworks/react-uswds' +import React, { useRef, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { DynamicStepIndicator } from '../../../../../components' +import { PageActionsContainer } from '../../../PageActions' +import styles from '../../../ReviewSubmit/ReviewSubmit.module.scss' +import { ActionButton } from '../../../../../components/ActionButton' +import { useStatePrograms } from '../../../../../hooks/useStatePrograms' +import { + RoutesRecord, + STATE_SUBMISSION_FORM_ROUTES, +} from '../../../../../constants' +import { useAuth } from '../../../../../contexts/AuthContext' +import { RateDetailsSummarySectionV2 } from './RateDetailsSummarySectionV2' +import { ContactsSummarySection } from './ContactsSummarySectionV2' +import { ContractDetailsSummarySectionV2 } from './ContractDetailsSummarySectionV2' +import { SubmissionTypeSummarySectionV2 } from './SubmissionTypeSummarySectionV2' +import { useFetchContractQuery } from '../../../../../gen/gqlClient' +import { ErrorForbiddenPage } from '../../../../Errors/ErrorForbiddenPage' +import { Error404 } from '../../../../Errors/Error404Page' +import { GenericErrorPage } from '../../../../Errors/GenericErrorPage' +import { Loading } from '../../../../../components' +import { PageBannerAlerts } from '../../../PageBannerAlerts' +import { packageName } from '../../../../../common-code/healthPlanFormDataType' + +type RouteParams = { + id: string +} +export const ReviewSubmitV2 = (): React.ReactElement => { + const navigate = useNavigate() + const modalRef = useRef(null) + const [isSubmitting] = useState(false) + // pull the programs off the user + const statePrograms = useStatePrograms() + const { loggedInUser } = useAuth() + + const { id } = useParams() + if (!id) { + throw new Error( + 'PROGRAMMING ERROR: id param not set in state submission form.' + ) + } + + const { data, loading, error } = useFetchContractQuery({ + variables: { + input: { + contractID: id, + }, + }, + fetchPolicy: 'network-only', + }) + + const contract = data?.fetchContract.contract + + if (loading) { + return ( + + + + ) + } else if (error || !contract) { + //error handling for a state user that tries to access rates for a different state + if (error?.graphQLErrors[0]?.extensions?.code === 'FORBIDDEN') { + return ( + + ) + } else if (error?.graphQLErrors[0]?.extensions?.code === 'NOT_FOUND') { + return + } else { + return + } + } + + // TODO to be removed once makeDocumentDateTable is updated to not rely on HPP and protos + const documentDateLookupTable = { + fakesha: 'Fri Mar 25 2022 16:13:20 GMT-0500 (Central Daylight Time)', + previousSubmissionDate: '01/01/01', + } + + const isContractActionAndRateCertification = + contract.draftRates && contract.draftRates.length > 0 + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + const programIDs = contractFormData.programIDs + const programs = statePrograms.filter((program) => + programIDs.includes(program.id) + ) + + const submissionName = packageName( + contract.stateCode, + contract.stateNumber, + contractFormData.programIDs, + programs + ) + return ( + <> +
+ + +
+ +
This is the V2 version of the Review Submit Page
+ + + + + {isContractActionAndRateCertification && ( + + )} + + + + + navigate(RoutesRecord.DASHBOARD_SUBMISSIONS) + } + disabled={isSubmitting} + > + Save as draft + + } + > + navigate('../documents')} + disabled={isSubmitting} + > + Back + + + Submit + + + + {/* // if the session is expiring, close this modal so the countdown modal can appear + */} +
+ + ) +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.stories.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.stories.tsx new file mode 100644 index 0000000000..95367b781c --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.stories.tsx @@ -0,0 +1,38 @@ +import { Story } from '@storybook/react' +import ProvidersDecorator from '../../../../../../.storybook/providersDecorator' +import { + SubmissionTypeSummarySectionV2Props, + SubmissionTypeSummarySectionV2, +} from './SubmissionTypeSummarySectionV2' +import { mockContractPackageDraft } from '../../../../../testHelpers/apolloMocks' + +export default { + title: 'Components/SubmissionSummary/SubmissionTypeSummarySection/V2', + component: SubmissionTypeSummarySectionV2, + parameters: { + componentSubtitle: + 'SubmissionTypeSummarySection displays the Submission Type data for a Draft or State Submission', + }, +} + +const Template: Story = (args) => ( + +) + +export const WithAction = Template.bind({}) +WithAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithAction.args = { + contract: mockContractPackageDraft(), + //TODO: Use better mock program data + statePrograms: [], + editNavigateTo: 'submission-type', +} + +export const WithoutAction = Template.bind({}) +WithoutAction.decorators = [(Story) => ProvidersDecorator(Story, {})] + +WithoutAction.args = { + contract: mockContractPackageDraft(), + statePrograms: [], +} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.test.tsx new file mode 100644 index 0000000000..982aeea634 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.test.tsx @@ -0,0 +1,211 @@ +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../../testHelpers/jestHelpers' +import { SubmissionTypeSummarySectionV2 as SubmissionTypeSummarySection } from './SubmissionTypeSummarySectionV2' +import { + mockContractPackageDraft, + mockMNState, + mockContractPackageSubmitted, +} from '../../../../../testHelpers/apolloMocks' + +describe('SubmissionTypeSummarySection', () => { + afterEach(() => { + jest.clearAllMocks() + }) + const draftContract = mockContractPackageDraft() + const stateSubmission = mockContractPackageSubmitted() + const statePrograms = mockMNState().programs + + it('can render draft package without errors', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'MN-PMAP-0001', + }) + ).toBeInTheDocument() + expect( + screen.getByRole('link', { name: 'Edit MN-PMAP-0001' }) + ).toHaveAttribute('href', '/submission-type') + + // Our mocks use the latest package data by default. + // Therefore we can check here that missing field is not being displayed unexpectedly + expect( + screen.queryByText(/You must provide this information/) + ).toBeNull() + }) + + it('can render submitted package without errors', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'MN-MSHO-0003', + }) + ).toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'Edit' })).toBeNull() + // We should never display missing field text on submission summary for submitted packages + expect( + screen.queryByText(/You must provide this information/) + ).toBeNull() + }) + + it('renders expected fields for draft package on review and submit', () => { + renderWithProviders( + + ) + + expect( + screen.getByRole('definition', { name: 'Program(s)' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: /Is this a risk based contract/, + }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Submission type' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Contract action type' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Submission description' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: /Which populations does this contract action cover\?/, + }) + ).toBeInTheDocument() + }) + + it('renders missing field message for population coverage question when expected', () => { + const draftContract = mockContractPackageDraft() + if (draftContract.draftRevision) { + draftContract.draftRevision.formData.populationCovered = undefined + + renderWithProviders( + + ) + } + expect( + screen.getByRole('definition', { + name: /Which populations does this contract action cover\?/, + }) + ).toBeInTheDocument() + const riskBasedDefinitionParentDiv = screen.getByRole('definition', { + name: /Which populations does this contract action cover\?/, + }) + if (!riskBasedDefinitionParentDiv) throw Error('Testing error') + expect(riskBasedDefinitionParentDiv).toHaveTextContent( + /You must provide this information/ + ) + }) + + it('renders missing field message for risk based contract when expected', () => { + const draftContract = mockContractPackageDraft() + if (draftContract.draftRevision) { + draftContract.draftRevision.formData.riskBasedContract = undefined + + renderWithProviders( + + ) + } + expect( + screen.getByRole('definition', { name: 'Program(s)' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { + name: /Is this a risk based contract/, + }) + ).toBeInTheDocument() + const riskBasedDefinitionParentDiv = screen.getByRole('definition', { + name: /Is this a risk based contract/, + }) + if (!riskBasedDefinitionParentDiv) throw Error('Testing error') + expect(riskBasedDefinitionParentDiv).toHaveTextContent( + /You must provide this information/ + ) + }) + + it('renders expected fields for submitted package on submission summary', () => { + renderWithProviders( + + ) + expect( + screen.getByRole('definition', { name: 'Program(s)' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Submission type' }) + ).toBeInTheDocument() + expect( + screen.getByRole('definition', { name: 'Submission description' }) + ).toBeInTheDocument() + expect( + screen.queryByRole('definition', { name: 'Submitted' }) + ).toBeInTheDocument() + }) + + it('does not render Submitted at field', () => { + renderWithProviders( + + ) + expect( + screen.queryByRole('definition', { name: 'Submitted' }) + ).not.toBeInTheDocument() + }) + + it('renders headerChildComponent component', () => { + renderWithProviders( + Test button} + submissionName="MN-PMAP-0001" + /> + ) + expect( + screen.queryByRole('button', { name: 'Test button' }) + ).toBeInTheDocument() + }) +}) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx new file mode 100644 index 0000000000..eb31f03841 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx @@ -0,0 +1,142 @@ +import { Grid } from '@trussworks/react-uswds' +import dayjs from 'dayjs' +import { DataDetail } from '../../../../../components/DataDetail' +import { DoubleColumnGrid } from '../../../../../components/DoubleColumnGrid' +import { SectionHeader } from '../../../../../components/SectionHeader' +import { + SubmissionTypeRecord, + ContractTypeRecord, + PopulationCoveredRecord, +} from '../../../../../constants/healthPlanPackages' +import { Program, Contract } from '../../../../../gen/gqlClient' +import { usePreviousSubmission } from '../../../../../hooks/usePreviousSubmission' +import { booleanAsYesNoUserValue } from '../../../../../components/Form/FieldYesNo/FieldYesNo' +import { SectionCard } from '../../../../../components/SectionCard' +import styles from '../../../../../components/SubmissionSummarySection/SubmissionSummarySection.module.scss' + +export type SubmissionTypeSummarySectionV2Props = { + contract: Contract + statePrograms: Program[] + editNavigateTo?: string + headerChildComponent?: React.ReactElement + subHeaderComponent?: React.ReactElement + initiallySubmittedAt?: Date + submissionName: string +} + +export const SubmissionTypeSummarySectionV2 = ({ + contract, + statePrograms, + editNavigateTo, + subHeaderComponent, + headerChildComponent, + initiallySubmittedAt, + submissionName, +}: SubmissionTypeSummarySectionV2Props): React.ReactElement => { + const isPreviousSubmission = usePreviousSubmission() + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + + const programNames = statePrograms + .filter((p) => contractFormData.programIDs.includes(p.id)) + .map((p) => p.name) + const isSubmitted = contract.status === 'SUBMITTED' + // const isRiskBasedContract = contractFormData.riskBasedContract === + return ( + + + {headerChildComponent && headerChildComponent} + + +
+ {isSubmitted && !isPreviousSubmission && ( + + + {dayjs(initiallySubmittedAt).format( + 'MM/DD/YY' + )} + + } + /> + <> + + )} + + + + + {contractFormData.riskBasedContract !== null && ( + + )} + + + + + + + + +
+
+ ) +} diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx index e71e0010a9..714520c2be 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx @@ -21,6 +21,7 @@ import { UnlockedHealthPlanFormDataType } from '../../common-code/healthPlanForm import { useLDClient } from 'launchdarkly-react-client-sdk' import { featureFlags } from '../../common-code/featureFlags' import { RateDetailsV2 } from './RateDetails/V2/RateDetailsV2' +import { ReviewSubmitV2 } from './ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2' import styles from './StateSubmissionForm.module.scss' // Can move this AppRoutes on future pass - leaving it here now to make diff clear @@ -72,7 +73,9 @@ export const StateSubmissionForm = (): React.ReactElement => { path={getRelativePathFromNestedRoute( 'SUBMISSIONS_REVIEW_SUBMIT' )} - element={} + element={ + useLinkedRates ? : + } /> } /> diff --git a/services/app-web/src/testHelpers/apolloMocks/contractGQLMock.ts b/services/app-web/src/testHelpers/apolloMocks/contractGQLMock.ts new file mode 100644 index 0000000000..deec7867f5 --- /dev/null +++ b/services/app-web/src/testHelpers/apolloMocks/contractGQLMock.ts @@ -0,0 +1,34 @@ +import { + FetchContractQuery, + Contract, + FetchContractDocument, +} from '../../gen/gqlClient' +import { MockedResponse } from '@apollo/client/testing' +import { mockContractPackageDraft } from './contractPackageDataMock' +import { GraphQLError } from 'graphql/index' + +const fetchContractMockSuccess = ({ + contract, +}: { + contract?: Partial +}): MockedResponse => { + const contractData = mockContractPackageDraft(contract) + + return { + request: { + query: FetchContractDocument, + variables: { input: { contractID: contractData.id } }, + }, + result: { + data: { + fetchContract: { + contract: { + ...contractData, + }, + }, + }, + }, + } +} + +export { fetchContractMockSuccess } diff --git a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts index cb86efa4d4..7cc463ff81 100644 --- a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts @@ -2,7 +2,9 @@ import { mockMNState } from '../../common-code/healthPlanFormDataMocks/healthPla import { Contract } from '../../gen/gqlClient' // Assemble versions of Contract data (with or without rates) for jest testing. Intended for use with related GQL Moc file. -function mockContractPackage(partial?: Partial): Contract { +function mockContractPackageDraft( + partial?: Partial +): Contract { return { status: 'DRAFT', createdAt: new Date(), @@ -13,27 +15,33 @@ function mockContractPackage(partial?: Partial): Contract { stateNumber: 5, draftRevision: { id: '123', - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date('01/01/2024'), + updatedAt: new Date('12/31/2024'), formData: { - programIDs: ['pmap'], + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], populationCovered: 'MEDICAID', submissionType: 'CONTRACT_AND_RATES', riskBasedContract: true, submissionDescription: 'A real submission', supportingDocuments: [], - stateContacts: [], + stateContacts: [ + { + name: 'State Contact 1', + titleRole: 'Test State Contact 1', + email: 'actuarycontact1@test.com', + }, + ], contractType: 'AMENDMENT', contractExecutionStatus: 'EXECUTED', contractDocuments: [ { - s3URL: 's3://bucketname/key/contract', + s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', - name: 'contract', + name: 'one two', }, ], - contractDateStart: new Date(), - contractDateEnd: new Date(), + contractDateStart: new Date('01/01/2023'), + contractDateEnd: new Date('12/31/2023'), managedCareEntities: ['MCO'], federalAuthorities: ['STATE_PLAN'], inLieuServicesAndSettings: true, @@ -53,9 +61,11 @@ function mockContractPackage(partial?: Partial): Contract { modifiedNetworkAdequacyStandards: true, modifiedLengthOfContract: false, modifiedNonRiskPaymentArrangements: true, - }, + statutoryRegulatoryAttestation: true, + statutoryRegulatoryAttestationDescription: "everything meets regulatory attestation" + } }, - + draftRates: [ { id: '123', @@ -74,16 +84,20 @@ function mockContractPackage(partial?: Partial): Contract { formData: { rateType: 'AMENDMENT', rateCapitationType: 'RATE_CELL', - rateDocuments: [], + rateDocuments: [ + { + s3URL: 's3://bucketname/key/rate', + sha256: 'fakesha', + name: 'rate', + }, + ], supportingDocuments: [], rateDateStart: new Date(), rateDateEnd: new Date(), rateDateCertified: new Date(), amendmentEffectiveDateStart: new Date(), amendmentEffectiveDateEnd: new Date(), - rateProgramIDs: [ - 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', - ], + rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], certifyingActuaryContacts: [ { actuarialFirm: 'DELOITTE', @@ -97,13 +111,14 @@ function mockContractPackage(partial?: Partial): Contract { actuarialFirm: 'DELOITTE', name: 'Actuary Contact 1', titleRole: 'Test Actuary Contact 1', - email: 'actuarycontact1@test.com', + email: 'additionalactuarycontact1@test.com', }, ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], - }, - }, + } + } + }, ], packageSubmissions: [], @@ -111,4 +126,118 @@ function mockContractPackage(partial?: Partial): Contract { } } -export { mockContractPackage } +function mockContractPackageSubmitted( + partial?: Partial +): Contract { + return { + status: 'SUBMITTED', + createdAt: new Date(), + updatedAt: new Date(), + id: 'test-abc-123', + stateCode: 'MN', + state: mockMNState(), + stateNumber: 5, + packageSubmissions: [{ + cause: 'CONTRACT_SUBMISSION', + submitInfo: { + updatedAt: new Date(), + updatedBy: 'example@state.com', + updatedReason: 'contract submit' + }, + submittedRevisions: [], + contractRevision: { + createdAt: new Date('01/01/2024'), + updatedAt: new Date('12/31/2024'), + id: '123', + formData: { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: true, + submissionDescription: 'A real submission', + supportingDocuments: [], + stateContacts: [], + contractType: 'AMENDMENT', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [ + { + s3URL: 's3://bucketname/key/contract', + sha256: 'fakesha', + name: 'contract', + }, + ], + contractDateStart: new Date(), + contractDateEnd: new Date(), + managedCareEntities: ['MCO'], + federalAuthorities: ['STATE_PLAN'], + inLieuServicesAndSettings: true, + modifiedBenefitsProvided: true, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: true, + modifiedRiskSharingStrategy: true, + modifiedIncentiveArrangements: false, + modifiedWitholdAgreements: false, + modifiedStateDirectedPayments: true, + modifiedPassThroughPayments: true, + modifiedPaymentsForMentalDiseaseInstitutions: false, + modifiedMedicalLossRatioStandards: true, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: true, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: true, + modifiedLengthOfContract: false, + modifiedNonRiskPaymentArrangements: true, + statutoryRegulatoryAttestation: true, + statutoryRegulatoryAttestationDescription: "everything meets regulatory attestation" + } + }, + rateRevisions: [ + { + id: '1234', + createdAt: new Date('01/01/2023'), + updatedAt: new Date('01/01/2023'), + contractRevisions: [], + formData: { + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://bucketname/key/rate', + sha256: 'fakesha', + name: 'rate', + }, + ], + supportingDocuments: [], + rateDateStart: new Date(), + rateDateEnd: new Date(), + rateDateCertified: new Date(), + amendmentEffectiveDateStart: new Date(), + amendmentEffectiveDateEnd: new Date(), + rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + certifyingActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Actuary Contact 1', + titleRole: 'Test Actuary Contact 1', + email: 'actuarycontact1@test.com', + }, + ], + addtlActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Actuary Contact 1', + titleRole: 'Test Actuary Contact 1', + email: 'actuarycontact1@test.com', + }, + ], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + packagesWithSharedRateCerts: [] + } + }, + ], + }], + ...partial, + } +} + +export { mockContractPackageDraft, mockContractPackageSubmitted } diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index eedef8ff7a..41b6094d68 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -21,7 +21,7 @@ import { domainToBase64, protoToBase64, } from '../../common-code/proto/healthPlanFormDataProto' -import { HealthPlanPackage, UpdateInformation } from '../../gen/gqlClient' +import { HealthPlanPackage, UpdateInformation, Contract, Rate } from '../../gen/gqlClient' import { mockMNState } from './stateMock' function mockDraft( diff --git a/services/app-web/src/testHelpers/apolloMocks/index.ts b/services/app-web/src/testHelpers/apolloMocks/index.ts index 9369dbeafa..8c0ef4483d 100644 --- a/services/app-web/src/testHelpers/apolloMocks/index.ts +++ b/services/app-web/src/testHelpers/apolloMocks/index.ts @@ -62,7 +62,10 @@ export { } from './apiKeyGQLMocks' // NEW APIS -export {mockContractPackage} from './contractPackageDataMock' +export { + mockContractPackageDraft, + mockContractPackageSubmitted, +} from './contractPackageDataMock' export { rateDataMock } from './rateDataMock' - +export { fetchContractMockSuccess } from './contractGQLMock' export { indexRatesMockSuccess, indexRatesMockFailure } from './rateGQLMocks' \ No newline at end of file diff --git a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts index 29d8bdf003..2b35def6f5 100644 --- a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts @@ -37,7 +37,7 @@ const contractRevisionOnRateDataMock = ( { __typename: 'StateContact', name: 'Name', - title: null, + titleRole: null, email: 'example@example.com', }, ],