From a8fccae0344f3b278101b891ca1c145bc6c291a1 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 10:18:53 -0500 Subject: [PATCH 01/22] Add statutoryRegulatoryAttestation field to DB. --- .../migration.sql | 6 ++++++ services/app-api/prisma/schema.prisma | 1 + 2 files changed, 7 insertions(+) create mode 100644 services/app-api/prisma/migrations/20231113040740_add_statutory_regulatory_attestation/migration.sql diff --git a/services/app-api/prisma/migrations/20231113040740_add_statutory_regulatory_attestation/migration.sql b/services/app-api/prisma/migrations/20231113040740_add_statutory_regulatory_attestation/migration.sql new file mode 100644 index 0000000000..c718b645a3 --- /dev/null +++ b/services/app-api/prisma/migrations/20231113040740_add_statutory_regulatory_attestation/migration.sql @@ -0,0 +1,6 @@ +BEGIN; + +-- AlterTable +ALTER TABLE "ContractRevisionTable" ADD COLUMN "statutoryRegulatoryAttestation" BOOLEAN; + +COMMIT; diff --git a/services/app-api/prisma/schema.prisma b/services/app-api/prisma/schema.prisma index 3f89e14ca5..9cae39f028 100644 --- a/services/app-api/prisma/schema.prisma +++ b/services/app-api/prisma/schema.prisma @@ -115,6 +115,7 @@ model ContractRevisionTable { modifiedLengthOfContract Boolean? modifiedNonRiskPaymentArrangements Boolean? inLieuServicesAndSettings Boolean? + statutoryRegulatoryAttestation Boolean? } model RateRevisionTable { From 02c0ba7e4a65b311a7e82471efdbd0fec65aefb4 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 10:28:32 -0500 Subject: [PATCH 02/22] Add statutoryRegulatoryAttestation to protobuf and domain types. --- .../convertContractWithRatesToHPP.ts | 2 ++ .../contractAndRates/formDataTypes.ts | 1 + .../prismaSharedContractRateHelpers.ts | 2 ++ .../contractAndRates/contractHelpers.ts | 1 + .../app-api/src/testHelpers/emailerHelpers.ts | 3 +++ .../app-proto/src/health_plan_form_data.proto | 1 + .../healthPlanFormData.ts | 1 + .../LockedHealthPlanFormDataType.ts | 1 + .../UnlockedHealthPlanFormDataType.ts | 1 + .../proto/healthPlanFormDataProto/toDomain.ts | 2 ++ .../healthPlanFormDataProto/toProtoBuffer.ts | 18 ++++++++++++++---- .../apolloMocks/healthPlanFormDataMock.ts | 2 ++ 12 files changed, 31 insertions(+), 4 deletions(-) diff --git a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts index dacdbee7db..40d6f0b1b3 100644 --- a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts +++ b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts @@ -231,6 +231,8 @@ function convertContractWithRatesToFormData( contractRev.formData.modifiedNonRiskPaymentArrangements, }, }, + statutoryRegulatoryAttestation: + contractRev.formData.statutoryRegulatoryAttestation, rateInfos, } diff --git a/services/app-api/src/domain-models/contractAndRates/formDataTypes.ts b/services/app-api/src/domain-models/contractAndRates/formDataTypes.ts index 630aa73b28..ec52c86414 100644 --- a/services/app-api/src/domain-models/contractAndRates/formDataTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/formDataTypes.ts @@ -64,6 +64,7 @@ const contractFormDataSchema = z.object({ modifiedNetworkAdequacyStandards: z.boolean().optional(), modifiedLengthOfContract: z.boolean().optional(), modifiedNonRiskPaymentArrangements: z.boolean().optional(), + statutoryRegulatoryAttestation: z.boolean().optional(), }) const rateFormDataSchema = z.object({ diff --git a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts index 18ed03cb70..41496233f6 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts @@ -339,6 +339,8 @@ function contractFormDataToDomainModel( contractRevision.modifiedNonRiskPaymentArrangements ?? undefined, inLieuServicesAndSettings: contractRevision.inLieuServicesAndSettings ?? undefined, + statutoryRegulatoryAttestation: + contractRevision.statutoryRegulatoryAttestation ?? undefined, } } diff --git a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts index baf1843361..58386dbf4d 100644 --- a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts +++ b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts @@ -206,6 +206,7 @@ const createContractRevision = ( inLieuServicesAndSettings: null, rateRevisions: [], draftRates: [], + statutoryRegulatoryAttestation: null, ...revision, } } diff --git a/services/app-api/src/testHelpers/emailerHelpers.ts b/services/app-api/src/testHelpers/emailerHelpers.ts index bb98df4211..13a6242c98 100644 --- a/services/app-api/src/testHelpers/emailerHelpers.ts +++ b/services/app-api/src/testHelpers/emailerHelpers.ts @@ -337,6 +337,7 @@ const mockContractAndRatesFormData = ( ], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', ...submissionPartial, + statutoryRegulatoryAttestation: false, } } @@ -520,6 +521,7 @@ const mockContractOnlyFormData = ( }, ], addtlActuaryContacts: [], + statutoryRegulatoryAttestation: false, ...submissionPartial, } } @@ -606,6 +608,7 @@ const mockContractAmendmentFormData = ( }, ], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', + statutoryRegulatoryAttestation: false, ...submissionPartial, } } diff --git a/services/app-proto/src/health_plan_form_data.proto b/services/app-proto/src/health_plan_form_data.proto index dbd9883393..126eb993c5 100644 --- a/services/app-proto/src/health_plan_form_data.proto +++ b/services/app-proto/src/health_plan_form_data.proto @@ -81,6 +81,7 @@ message ContractInfo { repeated FederalAuthority federal_authorities = 5; repeated Document contract_documents = 6; optional ContractExecutionStatus contract_execution_status = 7; + optional bool statutory_regulatory_attestation = 8; // Rates Refactor: No need for nested contract amendment info optional ContractAmendmentInfo contract_amendment_info = 50; diff --git a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts index 29d323ad48..451e982f85 100644 --- a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts +++ b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts @@ -763,6 +763,7 @@ function basicLockedHealthPlanFormData(): LockedHealthPlanFormDataType { programIDs: [mockMNState().programs[0].id], submissionType: 'CONTRACT_ONLY', riskBasedContract: false, + statutoryRegulatoryAttestation: false, submissionDescription: 'A real submission', documents: [], contractType: 'BASE', diff --git a/services/app-web/src/common-code/healthPlanFormDataType/LockedHealthPlanFormDataType.ts b/services/app-web/src/common-code/healthPlanFormDataType/LockedHealthPlanFormDataType.ts index 93ecad5dfc..a51771cd02 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/LockedHealthPlanFormDataType.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/LockedHealthPlanFormDataType.ts @@ -42,4 +42,5 @@ export type LockedHealthPlanFormDataType = { stateContacts: StateContact[] addtlActuaryContacts: ActuaryContact[] addtlActuaryCommunicationPreference?: ActuaryCommunicationType + statutoryRegulatoryAttestation: boolean } diff --git a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts index 437ef73644..a55547ad02 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts @@ -118,6 +118,7 @@ type UnlockedHealthPlanFormDataType = { federalAuthorities: FederalAuthority[] contractAmendmentInfo?: UnlockedContractAmendmentInfo rateInfos: RateInfoType[] + statutoryRegulatoryAttestation?: boolean } export type { diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts index 7afddfecdf..7be8b9534b 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts @@ -543,6 +543,8 @@ const toDomain = ( stateContacts: cleanedStateContacts, addtlActuaryContacts: parseActuaryContacts(addtlActuaryContacts), documents: parseProtoDocuments(formDataMessage.documents), + statutoryRegulatoryAttestation: + contractInfo?.statutoryRegulatoryAttestation ?? undefined, } // Now that we've gotten things into our combined draft & state domain format. diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts index e333fb2bbd..917314defb 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts @@ -65,7 +65,9 @@ function domainEnumStringToProtoString( return protoEnumString } -const domainDocsToProtoDocs = (domainDocs:SubmissionDocument[]): mcreviewproto.IDocument[] | null | undefined => { +const domainDocsToProtoDocs = ( + domainDocs: SubmissionDocument[] +): mcreviewproto.IDocument[] | null | undefined => { return domainDocs.map((doc) => ({ s3Url: doc.s3URL, name: doc.name, @@ -190,8 +192,12 @@ const toProtoBuffer = ( mcreviewproto.FederalAuthority, domainData.federalAuthorities ), - contractDocuments: domainDocsToProtoDocs(domainData.contractDocuments), + contractDocuments: domainDocsToProtoDocs( + domainData.contractDocuments + ), contractAmendmentInfo: contractAmendmentInfo, + statutoryRegulatoryAttestation: + domainData.statutoryRegulatoryAttestation, }, rateInfos: domainData.rateInfos && domainData.rateInfos.length @@ -215,8 +221,12 @@ const toProtoBuffer = ( rateDateCertified: domainDateToProtoDate( rateInfo.rateDateCertified ), - rateDocuments: domainDocsToProtoDocs(rateInfo.rateDocuments), - supportingDocuments: domainDocsToProtoDocs(rateInfo.supportingDocuments), + rateDocuments: domainDocsToProtoDocs( + rateInfo.rateDocuments + ), + supportingDocuments: domainDocsToProtoDocs( + rateInfo.supportingDocuments + ), rateCertificationName: generateRateName( domainData, rateInfo, diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index 7f6a5f077a..3d6af350ee 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -318,6 +318,7 @@ function mockStateSubmission(): LockedHealthPlanFormDataType { ], addtlActuaryContacts: [], addtlActuaryCommunicationPreference: undefined, + statutoryRegulatoryAttestation: false, } } @@ -417,6 +418,7 @@ function mockStateSubmissionContractAmendment(): LockedHealthPlanFormDataType { ], addtlActuaryContacts: [], addtlActuaryCommunicationPreference: undefined, + statutoryRegulatoryAttestation: false, } } From 41a5aea57f9413a99a70961c2014a895f8a7ecc0 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:39:14 -0500 Subject: [PATCH 03/22] Add statutoryRegulatoryAttestation to front-end zod and missed one mock. --- .../common-code/proto/healthPlanFormDataProto/zodSchemas.ts | 3 ++- .../src/testHelpers/apolloMocks/healthPlanFormDataMock.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts index 0b10fde7f1..4746556002 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts @@ -38,7 +38,7 @@ const submissionDocumentSchema = z.object({ .optional() ), sha256: z.string().optional(), - id: z.string().optional() // doesn't exist for newly created + id: z.string().optional(), // doesn't exist for newly created }) const contractAmendmentInfoSchema = z.object({ @@ -168,6 +168,7 @@ const unlockedHealthPlanFormDataZodSchema = z.object({ federalAuthorities: z.array(federalAuthoritySchema), contractAmendmentInfo: contractAmendmentInfoSchema.optional(), rateInfos: z.array(rateInfosTypeSchema), + statutoryRegulatoryAttestation: z.boolean().optional(), }) /* diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index 3d6af350ee..890aa93c03 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -211,6 +211,7 @@ function mockContractAndRatesDraft( }, ], addtlActuaryCommunicationPreference: 'OACT_TO_ACTUARY', + statutoryRegulatoryAttestation: true, ...partial, } } From fe0c22c16e2c8ce2edbc099f5f8349960b5d4d7b Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:40:40 -0500 Subject: [PATCH 04/22] Add feature flag. --- services/app-web/src/common-code/featureFlags/flags.ts | 4 ++++ services/cypress/support/launchDarklyCommands.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/app-web/src/common-code/featureFlags/flags.ts b/services/app-web/src/common-code/featureFlags/flags.ts index b25c088eda..e9f1f25051 100644 --- a/services/app-web/src/common-code/featureFlags/flags.ts +++ b/services/app-web/src/common-code/featureFlags/flags.ts @@ -76,6 +76,10 @@ const featureFlags = { flag: 'rate-filters', defaultValue: false, }, + CONTRACT_438_ATTESTATION: { + flag: '438-attestation', + defaultValue: false, + }, /** * Used in testing to simulate errors in fetching flag value. * This flag does not exist in LaunchDarkly dashboard so fetching this will return the defaultValue. diff --git a/services/cypress/support/launchDarklyCommands.ts b/services/cypress/support/launchDarklyCommands.ts index d70fea0d85..1e0c55d156 100644 --- a/services/cypress/support/launchDarklyCommands.ts +++ b/services/cypress/support/launchDarklyCommands.ts @@ -86,7 +86,8 @@ Cypress.Commands.add('stubFeatureFlags', () => { cy.interceptFeatureFlags({ 'packages-with-shared-rates': true, 'rates-db-refactor': true, - 'supporting-docs-by-rate': true + 'supporting-docs-by-rate': true, + '438-attestation': true }) }) From fcaa07e7fb9f58b4ff37879282d2e28f43d7ac29 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:41:34 -0500 Subject: [PATCH 05/22] Add attestation question to Contract Details page. --- .../statutoryRegulatoryAttestation.ts | 12 +++ .../ContractDetails/ContractDetails.tsx | 78 ++++++++++++++++++- .../ContractDetails/ContractDetailsSchema.ts | 7 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 services/app-web/src/constants/statutoryRegulatoryAttestation.ts diff --git a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts new file mode 100644 index 0000000000..71efcc110d --- /dev/null +++ b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts @@ -0,0 +1,12 @@ +const StatutoryRegulatoryAttestation = { + YES: 'Yes the contract fully complies with all applicable requirements.', + NO: 'No the contract does not fully comply with all applicable requirements', +} + +const StatutoryRegulatoryAttestationQuestion = + 'Do you attest that this contract complies with all applicable statutory and regulatory requirements including those contained in Title 42 Part 438 and Part 457 of the Code of Federal Regulations (CFR)?' + +export { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx index 9fe22fde41..a84354e805 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx @@ -62,6 +62,12 @@ import { isContractWithProvisions, } from '../../../common-code/healthPlanFormDataType/healthPlanFormData' import { RoutesRecord } from '../../../constants' +import { useLDClient } from 'launchdarkly-react-client-sdk' +import { featureFlags } from '../../../common-code/featureFlags' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../constants/statutoryRegulatoryAttestation' function formattedDatePlusOneDay(initialValue: string): string { const dayjsValue = dayjs(initialValue) @@ -113,6 +119,7 @@ export interface ContractDetailsFormValues { modifiedNetworkAdequacyStandards: string | undefined modifiedLengthOfContract: string | undefined modifiedNonRiskPaymentArrangements: string | undefined + statutoryRegulatoryAttestation: string | undefined } type FormError = FormikErrors[keyof FormikErrors] @@ -125,6 +132,12 @@ export const ContractDetails = ({ }: HealthPlanFormPageProps): React.ReactElement => { const [shouldValidate, setShouldValidate] = React.useState(showValidations) const navigate = useNavigate() + const ldClient = useLDClient() + + const contract438Attestation = ldClient?.variation( + featureFlags.CONTRACT_438_ATTESTATION.flag, + featureFlags.CONTRACT_438_ATTESTATION.defaultValue + ) // Contract documents state management const { deleteFile, uploadFile, scanFile, getKey, getS3URL } = useS3() @@ -322,6 +335,9 @@ export const ContractDetails = ({ draftSubmission?.contractAmendmentInfo?.modifiedProvisions .modifiedNonRiskPaymentArrangements ), + statutoryRegulatoryAttestation: formatForForm( + draftSubmission?.statutoryRegulatoryAttestation + ), } const showFieldErrors = (error?: FormError) => @@ -391,6 +407,9 @@ export const ContractDetails = ({ draftSubmission.managedCareEntities = values.managedCareEntities draftSubmission.federalAuthorities = values.federalAuthorities draftSubmission.contractDocuments = contractDocuments + draftSubmission.statutoryRegulatoryAttestation = formatYesNoForProto( + values.statutoryRegulatoryAttestation + ) if (isContractWithProvisions(draftSubmission)) { draftSubmission.contractAmendmentInfo = { @@ -480,7 +499,9 @@ export const ContractDetails = ({ : `../rate-details`, }) }} - validationSchema={() => ContractDetailsFormSchema(draftSubmission)} + validationSchema={() => + ContractDetailsFormSchema(draftSubmission, ldClient?.allFlags()) + } > {({ values, @@ -565,6 +586,61 @@ export const ContractDetails = ({ onFileItemsUpdate={onFileItemsUpdate} /> + {contract438Attestation && ( + +
+ + + { + StatutoryRegulatoryAttestationQuestion + } + + + + Required + + {showFieldErrors( + errors.statutoryRegulatoryAttestation + ) && ( + + { + errors.statutoryRegulatoryAttestation + } + + )} + + +
+
+ )} { const yesNoError = (provision: GeneralizedProvisionType) => { const noValidation = Yup.string().nullable() @@ -132,5 +134,8 @@ export const ContractDetailsFormSchema = ( modifiedNonRiskPaymentArrangements: yesNoError( 'modifiedNonRiskPaymentArrangements' ), + statutoryRegulatoryAttestation: activeFeatureFlags['438-attestation'] + ? Yup.string().defined('You must select yes or no') + : Yup.string(), }) } From 97253e0fb4c270d239f034d7933d51b4bf9132c3 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:43:11 -0500 Subject: [PATCH 06/22] Add attestation question to summary page. --- .../ContractDetailsSummarySection.tsx | 36 +++++++++++++++++++ .../SubmissionSummarySection.module.scss | 7 ++++ 2 files changed, 43 insertions(+) diff --git a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx index b360938a97..67947e6e18 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx @@ -34,6 +34,14 @@ import { DocumentDateLookupTableType } from '../../../documentHelpers/makeDocume import { recordJSException } from '../../../otelHelpers' import useDeepCompareEffect from 'use-deep-compare-effect' import { InlineDocumentWarning } from '../../DocumentWarning' +import { useLDClient } from 'launchdarkly-react-client-sdk' +import { featureFlags } from '../../../common-code/featureFlags' +import { Grid } from '@trussworks/react-uswds' +import { booleanAsYesNoFormValue } from '../../Form/FieldYesNo' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../constants/statutoryRegulatoryAttestation' export type ContractDetailsSummarySectionProps = { submission: HealthPlanFormDataType @@ -72,6 +80,17 @@ export const ContractDetailsSummarySection = ({ const [zippedFilesURL, setZippedFilesURL] = useState< string | undefined | Error >(undefined) + const ldClient = useLDClient() + + const contract438Attestation = ldClient?.variation( + featureFlags.CONTRACT_438_ATTESTATION.flag, + featureFlags.CONTRACT_438_ATTESTATION.defaultValue + ) + + const attestationYesNo = booleanAsYesNoFormValue( + submission.statutoryRegulatoryAttestation + ) + const contractSupportingDocuments = submission.documents.filter((doc) => doc.documentCategories.includes('CONTRACT_RELATED' as const) ) @@ -143,6 +162,23 @@ export const ContractDetailsSummarySection = ({ renderDownloadButton(zippedFilesURL)}
+ {contract438Attestation && ( + + + + + + )} Date: Mon, 13 Nov 2023 16:43:40 -0500 Subject: [PATCH 07/22] Fix unit test warnings and add tests for attestation question. --- .../ContractDetailsSummarySection.test.tsx | 146 +++++++++++----- .../ContractDetails/ContractDetails.test.tsx | 165 +++++++----------- 2 files changed, 171 insertions(+), 140 deletions(-) diff --git a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx index 3d223b520e..97e33b3f3b 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx @@ -1,5 +1,8 @@ import { screen, waitFor, within } from '@testing-library/react' -import { renderWithProviders } from '../../../testHelpers/jestHelpers' +import { + ldUseClientSpy, + renderWithProviders, +} from '../../../testHelpers/jestHelpers' import { ContractDetailsSummarySection } from './ContractDetailsSummarySection' import { fetchCurrentUserMock, @@ -8,8 +11,19 @@ import { } from '../../../testHelpers/apolloMocks' import { UnlockedHealthPlanFormDataType } from '../../../common-code/healthPlanFormDataType' import { testS3Client } from '../../../testHelpers/s3Helpers' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../constants/statutoryRegulatoryAttestation' describe('ContractDetailsSummarySection', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + const defaultApolloMocks = { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + } + it('can render draft submission without errors (review and submit behavior)', async () => { const testSubmission = { ...mockContractAndRatesDraft(), @@ -40,9 +54,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -76,7 +88,10 @@ describe('ContractDetailsSummarySection', () => { status: 'SUBMITTED', }} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) expect( @@ -101,14 +116,27 @@ describe('ContractDetailsSummarySection', () => { }) it('can render all contract details fields', () => { + ldUseClientSpy({ '438-attestation': true }) + const submission = mockContractAndRatesDraft({ + statutoryRegulatoryAttestation: true, + }) renderWithProviders( + />, + { + apolloProvider: defaultApolloMocks, + } ) + + expect( + screen.getByRole('definition', { + name: StatutoryRegulatoryAttestationQuestion, + }) + ).toBeInTheDocument() expect( screen.getByRole('definition', { name: 'Contract status' }) ).toBeInTheDocument() @@ -137,14 +165,49 @@ describe('ContractDetailsSummarySection', () => { ).toBeInTheDocument() }) - it('displays correct effective dates text for base contract', () => { + it('displays correct contract 438 attestation yes and no text', async () => { + ldUseClientSpy({ '438-attestation': true }) + const submission = mockContractAndRatesDraft({ + statutoryRegulatoryAttestation: true, + }) renderWithProviders( + />, + { + apolloProvider: defaultApolloMocks, + } ) + + const attestationYes = StatutoryRegulatoryAttestation.YES + + expect( + screen.getByRole('definition', { + name: StatutoryRegulatoryAttestationQuestion, + }) + ).toBeInTheDocument() + expect(await screen.findByText(attestationYes)).toBeInTheDocument() + }) + + it('displays correct effective dates text for base contract', async () => { + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider: defaultApolloMocks, + } + ) + }) + expect(screen.getByText('Contract effective dates')).toBeInTheDocument() }) @@ -154,7 +217,10 @@ describe('ContractDetailsSummarySection', () => { documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }} submission={mockContractAndRatesDraft()} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) expect( screen.getByText('Contract amendment effective dates') @@ -201,7 +267,10 @@ describe('ContractDetailsSummarySection', () => { documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }} submission={testSubmission} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) await waitFor(() => { @@ -243,7 +312,10 @@ describe('ContractDetailsSummarySection', () => { documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }} submission={mockContractAndRatesDraft()} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) expect( @@ -259,7 +331,10 @@ describe('ContractDetailsSummarySection', () => { documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }} submission={mockContractAndRatesDraft()} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) expect( screen.queryByRole('button', { @@ -287,9 +362,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -329,9 +402,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -370,6 +441,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { + apolloProvider: defaultApolloMocks, s3Provider, } ) @@ -389,7 +461,10 @@ describe('ContractDetailsSummarySection', () => { }} submission={mockContractAndRatesDraft()} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) expect( @@ -487,7 +562,10 @@ describe('ContractDetailsSummarySection', () => { contractType: 'BASE', })} submissionName="MN-PMAP-0001" - /> + />, + { + apolloProvider: defaultApolloMocks, + } ) const modifiedProvisions = screen.getByLabelText( @@ -541,9 +619,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) expect( @@ -619,9 +695,7 @@ describe('ContractDetailsSummarySection', () => { editNavigateTo="contract-details" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -670,9 +744,7 @@ describe('ContractDetailsSummarySection', () => { editNavigateTo="contract-details" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -720,9 +792,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -775,9 +845,7 @@ describe('ContractDetailsSummarySection', () => { editNavigateTo="contract-details" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) @@ -836,9 +904,7 @@ describe('ContractDetailsSummarySection', () => { submissionName="MN-PMAP-0001" />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloMocks, } ) diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx index 2c6121eac1..32494f0433 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx @@ -38,6 +38,10 @@ describe('ContractDetails', () => { ...mockDraft(), } + const defaultApolloProvider = { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + } + it('displays correct form guidance', async () => { renderWithProviders( { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) expect( @@ -69,9 +71,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -106,9 +106,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -129,9 +127,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -155,20 +151,27 @@ describe('ContractDetails', () => { }) describe('Federal authorities', () => { - it('displays correct form fields for federal authorities with medicaid contract', () => { - renderWithProviders( - - ) + it('displays correct form fields for federal authorities with medicaid contract', async () => { + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider: defaultApolloProvider, + } + ) + }) + const fedAuthQuestion = screen.getByRole('group', { name: 'Active federal operating authority', }) + expect(fedAuthQuestion).toBeInTheDocument() expect( within(fedAuthQuestion).getAllByRole('checkbox') @@ -189,7 +192,10 @@ describe('ContractDetails', () => { }} updateDraft={jest.fn()} previousDocuments={[]} - /> + />, + { + apolloProvider: defaultApolloProvider, + } ) const fedAuthQuestion = await screen.findByRole('group', { name: 'Active federal operating authority', @@ -233,9 +239,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) await screen.findByRole('form') @@ -281,9 +285,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) // trigger validations @@ -343,9 +345,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) await screen.findByRole('form') @@ -378,9 +378,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -433,9 +431,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) await screen.findByRole('form') @@ -457,9 +453,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) await screen.findByRole('form') @@ -501,9 +495,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -566,9 +558,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -592,9 +582,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -623,9 +611,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -634,7 +620,7 @@ describe('ContractDetails', () => { }) expect(continueButton).not.toHaveAttribute('aria-disabled') - continueButton.click() + await userEvent.click(continueButton) await waitFor(() => { expect( @@ -653,9 +639,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -667,9 +651,10 @@ describe('ContractDetails', () => { await userEvent.upload(input, [TEST_DOC_FILE]) await userEvent.upload(input, []) // clear input and ensure we add same file twice await userEvent.upload(input, [TEST_DOC_FILE]) + expect(continueButton).not.toHaveAttribute('aria-disabled') + await userEvent.click(continueButton) - continueButton.click() await waitFor(() => { expect( screen.getAllByText( @@ -689,9 +674,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) const continueButton = screen.getByRole('button', { @@ -706,7 +689,7 @@ describe('ContractDetails', () => { ).toBeInTheDocument() expect(continueButton).not.toHaveAttribute('aria-disabled') - continueButton.click() + await userEvent.click(continueButton) expect( await screen.findAllByText( @@ -724,9 +707,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) const continueButton = screen.getByRole('button', { @@ -749,14 +730,16 @@ describe('ContractDetails', () => { 'file-input-preview-image' )[1] expect(imageElFile2).toHaveClass('is-loading') - fireEvent.click(continueButton) - expect(continueButton).toHaveAttribute('aria-disabled', 'true') + await waitFor(() => { + fireEvent.click(continueButton) + expect(continueButton).toHaveAttribute('aria-disabled', 'true') - expect( - screen.getAllByText( - 'You must wait for all documents to finish uploading before continuing' - ) - ).toHaveLength(2) + expect( + screen.getAllByText( + 'You must wait for all documents to finish uploading before continuing' + ) + ).toHaveLength(2) + }) }) }) @@ -770,9 +753,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -797,9 +778,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -826,9 +805,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -864,9 +841,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -891,9 +866,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) const input = screen.getByLabelText('Upload contract') @@ -931,9 +904,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -957,9 +928,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -986,9 +955,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) @@ -1013,9 +980,7 @@ describe('ContractDetails', () => { previousDocuments={[]} />, { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, + apolloProvider: defaultApolloProvider, } ) From 4af78e7b75bb751797f1958b58996da3084d0434 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:44:19 -0500 Subject: [PATCH 08/22] Add statutoryRegulatoryAttestation to postgres updateDraftContractWithRates handler. --- .../postgres/contractAndRates/updateDraftContractWithRates.ts | 4 ++++ .../healthPlanPackage/updateHealthPlanFormData.test.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts index 77b0092ee6..4e11935a89 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts @@ -130,6 +130,7 @@ async function updateDraftContractWithRates( modifiedLengthOfContract, modifiedNonRiskPaymentArrangements, inLieuServicesAndSettings, + statutoryRegulatoryAttestation, } = formData try { @@ -504,6 +505,9 @@ async function updateDraftContractWithRates( modifiedNonRiskPaymentArrangements: nullify( modifiedNonRiskPaymentArrangements ), + statutoryRegulatoryAttestation: nullify( + statutoryRegulatoryAttestation + ), draftRates: { disconnect: updateRates?.disconnectRates ? updateRates.disconnectRates.map((rate) => ({ diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts index b957e1cf29..bd4be402af 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts @@ -101,6 +101,7 @@ describe.each(flagValueTestParameters)( modifiedNonRiskPaymentArrangements: undefined, }, }, + statutoryRegulatoryAttestation: true, rateInfos: [], }) From d625f0de8d04999ca8b55097316649b67bc88680 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 16:45:01 -0500 Subject: [PATCH 09/22] Update cypress test commands. --- services/cypress/support/stateSubmissionFormCommands.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index 2fca38629d..6fb3917347 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -99,6 +99,9 @@ Cypress.Commands.add('fillOutContractActionAndRateCertification', () => { }) Cypress.Commands.add('fillOutBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' + // Contract 438 attestation question + cy.findByText('Yes the contract fully complies with all applicable requirements.').click() + cy.findByText('Fully executed').click() cy.findAllByLabelText('Start date', {timeout: 2000}) .parents() From 6ac919b563c87af7c03ff915611a5f3b1e3cf9d5 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 17:57:21 -0500 Subject: [PATCH 10/22] Missed some more mock data. --- services/app-api/src/testHelpers/gqlHelpers.ts | 1 + services/cypress/utils/apollo-test-utils.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/app-api/src/testHelpers/gqlHelpers.ts b/services/app-api/src/testHelpers/gqlHelpers.ts index f28158f113..725dcdce2d 100644 --- a/services/app-api/src/testHelpers/gqlHelpers.ts +++ b/services/app-api/src/testHelpers/gqlHelpers.ts @@ -299,6 +299,7 @@ const createAndUpdateTestHealthPlanPackage = async ( modifiedNonRiskPaymentArrangements: true, }, } + draft.statutoryRegulatoryAttestation = true Object.assign(draft, partialUpdates) diff --git a/services/cypress/utils/apollo-test-utils.ts b/services/cypress/utils/apollo-test-utils.ts index 6fb2f85fc3..ab5b43bab4 100644 --- a/services/cypress/utils/apollo-test-utils.ts +++ b/services/cypress/utils/apollo-test-utils.ts @@ -100,6 +100,7 @@ const contractOnlyData = (): Partial=> ({ managedCareEntities: ['MCO'], federalAuthorities: ['STATE_PLAN'], rateInfos: [], + statutoryRegulatoryAttestation: true }) const contractAndRatesData = (): Partial=> ({ @@ -200,9 +201,8 @@ const contractAndRatesData = (): Partial=> ({ actuaryCommunicationPreference: 'OACT_TO_ACTUARY' as const, packagesWithSharedRateCerts: [], }, - ] - - + ], + statutoryRegulatoryAttestation: true }) const newSubmissionInput = (overrides?: Partial ): Partial => { From c237159c53bcd032f402ee1203b5d8dfa14a57b3 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 18:07:57 -0500 Subject: [PATCH 11/22] Validate statutoryRegulatoryAttestation on submit. --- .../submitHealthPlanPackage.test.ts | 75 ++++++++++++++++++- .../submitHealthPlanPackage.ts | 36 +++++++-- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts index ac886eecd2..8904376bec 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts @@ -57,10 +57,12 @@ describe.each(flagValueTestParameters)( `Tests $testName`, ({ flagName, flagValue }) => { const cmsUser = testCMSUser() - const mockLDService = testLDService({ [flagName]: flagValue }) + const mockLDService = testLDService({ + [flagName]: flagValue, + }) afterEach(() => { - jest.clearAllMocks() + jest.restoreAllMocks() }) it('returns a StateSubmission if complete', async () => { const server = await constructTestPostgresServer({ @@ -1252,5 +1254,74 @@ describe.each(flagValueTestParameters)( ) }, 20000) }) + + describe('Feature flagged 4348 attestation question test', () => { + const ldService = testLDService({ + ...mockLDService, + '438-attestation': true, + }) + + it('errors when contract 4348 attestation question is undefined', async () => { + const server = await constructTestPostgresServer({ + ldService: ldService, + }) + + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + statutoryRegulatoryAttestation: undefined, + } + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id + + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, + }) + + expect(submitResult.errors).toBeDefined() + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }, 20000) + it('successfully submits when contract 4348 attestation question is valid', async () => { + const server = await constructTestPostgresServer({ + ldService: ldService, + }) + + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + statutoryRegulatoryAttestation: false, + } + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id + + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, + }) + + expect(submitResult.errors).toBeUndefined() + }, 20000) + }) } ) diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts index b74e363735..c3cbab5fb7 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts @@ -36,7 +36,10 @@ import type { HealthPlanFormDataType, LockedHealthPlanFormDataType, } from '../../../../app-web/src/common-code/healthPlanFormDataType' -import type { LDService } from '../../launchDarkly/launchDarkly' +import type { + FeatureFlagSettings, + LDService, +} from '../../launchDarkly/launchDarkly' import { convertContractWithRatesToFormData, convertContractWithRatesToUnlockedHPP, @@ -106,7 +109,8 @@ const validateStatusAndUpdateInfo = ( // This strategy (returning a different type from validation) is taken from the // "parse, don't validate" article: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ export function parseAndSubmit( - draft: HealthPlanFormDataType + draft: HealthPlanFormDataType, + featureFlag: FeatureFlagSettings ): LockedHealthPlanFormDataType | SubmissionError { // Remove fields from edits on irrelevant logic branches // - CONTRACT_ONLY submission type should not contain any CONTRACT_AND_RATE rates data. @@ -125,10 +129,22 @@ export function parseAndSubmit( submittedAt: new Date(), } - if (isValidAndCurrentLockedHealthPlanFormData(maybeStateSubmission)) + // Check for valid attestation. Returns true if flag is off. + const isValid438Attestation = !featureFlag['438-attestation'] + ? true + : Boolean(featureFlag['438-attestation']) && + draft.statutoryRegulatoryAttestation !== undefined + + if ( + isValidAndCurrentLockedHealthPlanFormData(maybeStateSubmission) && + isValid438Attestation + ) return maybeStateSubmission else if ( - !hasValidContract(maybeStateSubmission as LockedHealthPlanFormDataType) + !hasValidContract( + maybeStateSubmission as LockedHealthPlanFormDataType + ) || + !isValid438Attestation ) { return { code: 'INCOMPLETE', @@ -184,6 +200,10 @@ export function submitHealthPlanPackageResolver( context, 'rates-db-refactor' ) + const contract438Attestation = await launchDarkly.getFeatureFlag( + context, + '438-attestation' + ) const { user, span } = context const { submittedReason, pkgID } = input setResolverDetailsOnActiveSpan('submitHealthPlanPackage', user, span) @@ -307,7 +327,9 @@ export function submitHealthPlanPackageResolver( contractRevisionID = contractWithHistory.draftRevision.id // Final clean + check of data before submit - parse to state submission - const maybeLocked = parseAndSubmit(initialFormData) + const maybeLocked = parseAndSubmit(initialFormData, { + '438-attestation': contract438Attestation, + }) if (isSubmissionError(maybeLocked)) { const errMessage = maybeLocked.message @@ -530,7 +552,9 @@ export function submitHealthPlanPackageResolver( contractRevisionID = initialPackage.revisions[0].id // Final clean + check of data before submit - parse to state submission - const maybeLocked = parseAndSubmit(initialFormData) + const maybeLocked = parseAndSubmit(initialFormData, { + '438-attestation': contract438Attestation, + }) if (isSubmissionError(maybeLocked)) { const errMessage = maybeLocked.message From ac975887fdf1ef029c741e45e9b81e22089108ab Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 19:09:18 -0500 Subject: [PATCH 12/22] Missed handler. --- .../app-api/src/postgres/contractAndRates/unlockContract.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index 2628919594..2cda27d7d1 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -141,6 +141,8 @@ async function unlockContract( currentRev.modifiedLengthOfContract, modifiedNonRiskPaymentArrangements: currentRev.modifiedNonRiskPaymentArrangements, + statutoryRegulatoryAttestation: + currentRev.statutoryRegulatoryAttestation, contractDocuments: { create: currentRev.contractDocuments.map((d) => ({ From 56f72004640456d9c5a658f9ec3dc2fe8a42198b Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 19:43:02 -0500 Subject: [PATCH 13/22] Mock `allFlags` method. --- services/app-web/src/testHelpers/jestHelpers.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/app-web/src/testHelpers/jestHelpers.tsx b/services/app-web/src/testHelpers/jestHelpers.tsx index cd8b68ed16..278369d6c0 100644 --- a/services/app-web/src/testHelpers/jestHelpers.tsx +++ b/services/app-web/src/testHelpers/jestHelpers.tsx @@ -23,6 +23,8 @@ import { FeatureFlagLDConstant, FlagValue, FeatureFlagSettings, + featureFlagKeys, + featureFlags, } from '../common-code/featureFlags' /* Render */ @@ -77,6 +79,13 @@ const WithLocation = ({ return null } +const getDefaultFeatureFlags = (): FeatureFlagSettings => + featureFlagKeys.reduce((a, c) => { + const flag = featureFlags[c].flag + const defaultValue = featureFlags[c].defaultValue + return Object.assign(a, { [flag]: defaultValue }) + }, {} as FeatureFlagSettings) + //WARNING: This required tests using this function to clear mocks afterwards. const ldUseClientSpy = (featureFlags: FeatureFlagSettings) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -109,6 +118,11 @@ const ldUseClientSpy = (featureFlags: FeatureFlagSettings) => { ? defaultValue : featureFlags[flag] }, + allFlags: () => { + const defaultFeatureFlags = getDefaultFeatureFlags() + Object.assign(defaultFeatureFlags, featureFlags) + return defaultFeatureFlags + }, } }) } From 0b24aa2cd78baac541896160146b1b9120e3305f Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 19:45:24 -0500 Subject: [PATCH 14/22] Add 438 attestation tests to ContractDetails --- .../ContractDetails/ContractDetails.test.tsx | 96 +++++++++++++++++++ .../apolloMocks/healthPlanFormDataMock.ts | 1 + 2 files changed, 97 insertions(+) diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx index 32494f0433..fbf7565821 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event' import { mockDraft, fetchCurrentUserMock, + mockBaseContract, } from '../../../testHelpers/apolloMocks' import { @@ -15,6 +16,7 @@ import { TEST_PNG_FILE, dragAndDrop, selectYesNoRadio, + ldUseClientSpy, } from '../../../testHelpers/jestHelpers' import { ACCEPTED_SUBMISSION_FILE_TYPES } from '../../../components/FileUpload' import { ContractDetails } from './' @@ -25,6 +27,10 @@ import { modifiedProvisionMedicaidAmendmentKeys, modifiedProvisionMedicaidBaseKeys, } from '../../../common-code/healthPlanFormDataType' +import { + StatutoryRegulatoryAttestation, + StatutoryRegulatoryAttestationQuestion, +} from '../../../constants/statutoryRegulatoryAttestation' const scrollIntoViewMock = jest.fn() HTMLElement.prototype.scrollIntoView = scrollIntoViewMock @@ -1020,4 +1026,94 @@ describe('ContractDetails', () => { ) }) }) + + describe('Contract 438 attestation', () => { + it('renders 438 attestation question errors', async () => { + ldUseClientSpy({ '438-attestation': true }) + const draft = mockBaseContract({ + statutoryRegulatoryAttestation: true, + }) + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider: defaultApolloProvider, + } + ) + }) + + // expect 438 attestation question to be on the page + expect( + screen.getByText(StatutoryRegulatoryAttestationQuestion) + ).toBeInTheDocument() + + const yesRadio = screen.getByRole('radio', { + name: StatutoryRegulatoryAttestation.YES, + }) + const noRadio = screen.getByRole('radio', { + name: StatutoryRegulatoryAttestation.YES, + }) + + // expect both yes and no answers on the page and yes to be checked + expect(yesRadio).toBeChecked() + expect(noRadio).toBeInTheDocument() + }) + it('errors when continuing without answering 438 attestation question', async () => { + ldUseClientSpy({ '438-attestation': true }) + const draft = mockBaseContract({ + statutoryRegulatoryAttestation: undefined, + }) + const mockUpdateDraftFn = jest.fn() + await waitFor(() => { + renderWithProviders( + , + { + apolloProvider: defaultApolloProvider, + } + ) + }) + + // expect 438 attestation question to be on the page + expect( + screen.getByText(StatutoryRegulatoryAttestationQuestion) + ).toBeInTheDocument() + + const yesRadio = screen.getByRole('radio', { + name: StatutoryRegulatoryAttestation.YES, + }) + const noRadio = screen.getByRole('radio', { + name: StatutoryRegulatoryAttestation.YES, + }) + + // expect both yes and no answers on the page and yes to be checked + expect(yesRadio).toBeInTheDocument() + expect(noRadio).toBeInTheDocument() + + // check no radio + await userEvent.click(noRadio) + + const continueButton = screen.getByRole('button', { + name: 'Continue', + }) + + // click continue + await userEvent.click(continueButton) + + // expect errors for attestation question + await waitFor(() => { + expect(mockUpdateDraftFn).not.toHaveBeenCalled() + expect( + screen.queryAllByText('You must select yes or no') + ).toHaveLength(2) + }) + }) + }) }) diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index 890aa93c03..157f4ddd16 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -116,6 +116,7 @@ function mockBaseContract( ], addtlActuaryContacts: [], addtlActuaryCommunicationPreference: undefined, + statutoryRegulatoryAttestation: true, ...partial, } } From d5051fb75cf93c67c1bcd8e970a66c9088c7a1bc Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 19:45:54 -0500 Subject: [PATCH 15/22] Fix test name. --- .../StateSubmission/ContractDetails/ContractDetails.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx index fbf7565821..666b5ed7d2 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx @@ -1028,7 +1028,7 @@ describe('ContractDetails', () => { }) describe('Contract 438 attestation', () => { - it('renders 438 attestation question errors', async () => { + it('renders 438 attestation question without errors', async () => { ldUseClientSpy({ '438-attestation': true }) const draft = mockBaseContract({ statutoryRegulatoryAttestation: true, From 2e86e0a0a6e4392c60dd93a41027a7b626e808a3 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Mon, 13 Nov 2023 20:30:59 -0500 Subject: [PATCH 16/22] Update one more cypress command --- .../app-web/src/constants/statutoryRegulatoryAttestation.ts | 2 +- services/cypress/support/stateSubmissionFormCommands.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts index 71efcc110d..13b2e7246d 100644 --- a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts +++ b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts @@ -1,6 +1,6 @@ const StatutoryRegulatoryAttestation = { YES: 'Yes the contract fully complies with all applicable requirements.', - NO: 'No the contract does not fully comply with all applicable requirements', + NO: 'No the contract does not fully comply with all applicable requirements.', } const StatutoryRegulatoryAttestationQuestion = diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index 6fb3917347..db9433033e 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -177,6 +177,9 @@ Cypress.Commands.add('fillOutBaseContractDetails', () => { Cypress.Commands.add('fillOutAmendmentToBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' + // Contract 438 attestation question + cy.findByText('No the contract does not fully comply with all applicable requirements.').click() + cy.findByText('Unexecuted by some or all parties').click() cy.findAllByLabelText('Start date', {timeout: 2000}) From 4837f2f080f8400d689ccc39d5d7fa6cfcd99533 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 14 Nov 2023 12:56:36 -0500 Subject: [PATCH 17/22] Add links to guides. --- .../ContractDetails/ContractDetails.tsx | 40 +++++++++++++++++-- .../StateSubmissionForm.module.scss | 7 ++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx index a84354e805..33c45bd92d 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx @@ -604,13 +604,45 @@ export const ContractDetails = ({ } - - Required - + + Required + + + + Managed Care Contract Review + and Approval State Guide + + + CHIP Managed Care Contract + Review and Approval State + Guide + + + {showFieldErrors( errors.statutoryRegulatoryAttestation ) && ( diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss index 002c9f1f8d..e8c4cb5ff7 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss @@ -149,3 +149,10 @@ display: flex; flex-direction: column; } + +.contractAttestationHint { + margin-bottom: units(3); + a { + margin-top: units(1); + } +} From a0d18e200bbe381d57694d993334df24579b9ab6 Mon Sep 17 00:00:00 2001 From: Jason Lin <98117700+JasonLin0991@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:23:30 -0500 Subject: [PATCH 18/22] Update services/app-web/src/constants/statutoryRegulatoryAttestation.ts Co-authored-by: haworku --- .../app-web/src/constants/statutoryRegulatoryAttestation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts index 13b2e7246d..1b1af08445 100644 --- a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts +++ b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts @@ -1,6 +1,6 @@ const StatutoryRegulatoryAttestation = { - YES: 'Yes the contract fully complies with all applicable requirements.', - NO: 'No the contract does not fully comply with all applicable requirements.', + YES: 'Yes, the contract fully complies with all applicable requirements', + NO: 'No, the contract does not fully comply with all applicable requirements', } const StatutoryRegulatoryAttestationQuestion = From 414ad9be988273c6936d7e0039a684f068841b7f Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 14 Nov 2023 15:36:05 -0500 Subject: [PATCH 19/22] Move attestation question error to the top. --- .../ContractDetails/ContractDetailsSchema.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts index 045c3bb183..5ef2c6871d 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts @@ -52,7 +52,11 @@ export const ContractDetailsFormSchema = ( } } + // Errors in the error summary is ordered by the position of each ket in the yup object. return Yup.object().shape({ + statutoryRegulatoryAttestation: activeFeatureFlags['438-attestation'] + ? Yup.string().defined('You must select yes or no') + : Yup.string(), contractExecutionStatus: Yup.string().defined( 'You must select a contract status' ), @@ -134,8 +138,5 @@ export const ContractDetailsFormSchema = ( modifiedNonRiskPaymentArrangements: yesNoError( 'modifiedNonRiskPaymentArrangements' ), - statutoryRegulatoryAttestation: activeFeatureFlags['438-attestation'] - ? Yup.string().defined('You must select yes or no') - : Yup.string(), }) } From 0d20159c5dca260f22e6a64bf149cd85f951a5ec Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 14 Nov 2023 16:16:28 -0500 Subject: [PATCH 20/22] Fix test for text change. --- services/cypress/support/stateSubmissionFormCommands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index db9433033e..f14d12cf71 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -100,7 +100,7 @@ Cypress.Commands.add('fillOutContractActionAndRateCertification', () => { Cypress.Commands.add('fillOutBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' // Contract 438 attestation question - cy.findByText('Yes the contract fully complies with all applicable requirements.').click() + cy.findByText('Yes the contract fully complies with all applicable requirements').click() cy.findByText('Fully executed').click() cy.findAllByLabelText('Start date', {timeout: 2000}) @@ -178,7 +178,7 @@ Cypress.Commands.add('fillOutBaseContractDetails', () => { Cypress.Commands.add('fillOutAmendmentToBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' // Contract 438 attestation question - cy.findByText('No the contract does not fully comply with all applicable requirements.').click() + cy.findByText('No the contract does not fully comply with all applicable requirements').click() cy.findByText('Unexecuted by some or all parties').click() From 7ad9b8630049ad873f89186483002763cb9cf397 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 14 Nov 2023 16:53:32 -0500 Subject: [PATCH 21/22] Fix test for text change. --- services/cypress/support/stateSubmissionFormCommands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index f14d12cf71..9419910183 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -100,7 +100,7 @@ Cypress.Commands.add('fillOutContractActionAndRateCertification', () => { Cypress.Commands.add('fillOutBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' // Contract 438 attestation question - cy.findByText('Yes the contract fully complies with all applicable requirements').click() + cy.findByText('Yes, the contract fully complies with all applicable requirements').click() cy.findByText('Fully executed').click() cy.findAllByLabelText('Start date', {timeout: 2000}) @@ -178,7 +178,7 @@ Cypress.Commands.add('fillOutBaseContractDetails', () => { Cypress.Commands.add('fillOutAmendmentToBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' // Contract 438 attestation question - cy.findByText('No the contract does not fully comply with all applicable requirements').click() + cy.findByText('No, the contract does not fully comply with all applicable requirements').click() cy.findByText('Unexecuted by some or all parties').click() From 46ac5fcda606fd0d4ef7363c1c0a1b674502f6ad Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 14 Nov 2023 17:44:02 -0500 Subject: [PATCH 22/22] cypress re-run