diff --git a/services/app-api/src/common-code/featureFlags/flags.ts b/services/app-api/src/common-code/featureFlags/flags.ts index 32af51befc..61bac0122f 100644 --- a/services/app-api/src/common-code/featureFlags/flags.ts +++ b/services/app-api/src/common-code/featureFlags/flags.ts @@ -10,7 +10,7 @@ const featureFlags = { flag: '438-attestation', defaultValue: false, }, - /** + /** * Enables state and CMS rate edit, unlock, resubmit functionality */ RATE_EDIT_UNLOCK: { @@ -22,7 +22,7 @@ const featureFlags = { /** Enables the modal that alerts the user to an expiring session */ - SESSION_EXPIRING_MODAL: { + SESSION_EXPIRING_MODAL: { flag: 'session-expiring-modal', defaultValue: true, }, @@ -36,7 +36,7 @@ const featureFlags = { /** Toggles the site maintenance alert on the landing page */ - SITE_UNDER_MAINTENANCE_BANNER: { + SITE_UNDER_MAINTENANCE_BANNER: { flag: 'site-under-maintenance-banner', defaultValue: 'OFF', }, diff --git a/services/app-web/src/common-code/ContractType.ts b/services/app-web/src/common-code/ContractType.ts index 4cb3bc6278..8c65cf10bd 100644 --- a/services/app-web/src/common-code/ContractType.ts +++ b/services/app-web/src/common-code/ContractType.ts @@ -1,43 +1,43 @@ -import { Contract, ContractRevision } from '../gen/gqlClient' +import { Contract, ContractRevision, UnlockedContract } from '../gen/gqlClient' import { getLastContractSubmission } from '../gqlHelpers/contractsAndRates' -const getContractRev = (contract: Contract): ContractRevision | undefined => { +const getContractRev = (contract: Contract | UnlockedContract): ContractRevision | undefined => { if (contract.draftRevision) { return contract.draftRevision } else { return getLastContractSubmission(contract)?.contractRevision } } -const isContractOnly = (contract: Contract): boolean => { +const isContractOnly = (contract: Contract | UnlockedContract): boolean => { const contractRev = getContractRev(contract) return contractRev?.formData?.submissionType === 'CONTRACT_ONLY' } -const isBaseContract = (contract: Contract): boolean => { +const isBaseContract = (contract: Contract | UnlockedContract): boolean => { const contractRev = getContractRev(contract) return contractRev?.formData?.contractType === 'BASE' } -const isContractAmendment = (contract: Contract): boolean => { +const isContractAmendment = (contract: Contract | UnlockedContract): boolean => { const contractRev = getContractRev(contract) return contractRev?.formData?.contractType === 'AMENDMENT' } -const isCHIPOnly = (contract: Contract): boolean => { +const isCHIPOnly = (contract: Contract | UnlockedContract): boolean => { const contractRev = getContractRev(contract) return contractRev?.formData?.populationCovered === 'CHIP' } -const isContractAndRates = (contract: Contract): boolean => { +const isContractAndRates = (contract: Contract | UnlockedContract): boolean => { const contractRev = getContractRev(contract) return contractRev?.formData?.submissionType === 'CONTRACT_AND_RATES' } -const isContractWithProvisions = (contract: Contract): boolean => +const isContractWithProvisions = (contract: Contract | UnlockedContract): boolean => isContractAmendment(contract) || (isBaseContract(contract) && !isCHIPOnly(contract)) -const isSubmitted = (contract: Contract): boolean => +const isSubmitted = (contract: Contract | UnlockedContract): boolean => contract.status === 'SUBMITTED' export { diff --git a/services/app-web/src/common-code/ContractTypeProvisions.ts b/services/app-web/src/common-code/ContractTypeProvisions.ts index bf618bced9..906f11c14d 100644 --- a/services/app-web/src/common-code/ContractTypeProvisions.ts +++ b/services/app-web/src/common-code/ContractTypeProvisions.ts @@ -3,7 +3,7 @@ import { ModifiedProvisionsBaseContractRecord, ModifiedProvisionsCHIPRecord, } from '../constants/modifiedProvisions' -import { Contract } from '../gen/gqlClient' +import { Contract, UnlockedContract } from '../gen/gqlClient' import { CHIPProvisionType, MedicaidBaseProvisionType, @@ -38,7 +38,7 @@ import { getLastContractSubmission } from '../gqlHelpers/contractsAndRates' // Returns the list of provision keys that apply for given submission variant const generateApplicableProvisionsList = ( - draftSubmission: Contract + draftSubmission: Contract | UnlockedContract ): | CHIPProvisionType[] | MedicaidBaseProvisionType[] @@ -56,7 +56,7 @@ const generateApplicableProvisionsList = ( // Returns user-friendly label text for the provision based on the given submission variant const generateProvisionLabel = ( - draftSubmission: Contract, + draftSubmission: Contract | UnlockedContract, provision: GeneralizedProvisionType ): string => { if (isCHIPOnly(draftSubmission) && isCHIPProvision(provision)) { @@ -83,7 +83,7 @@ const generateProvisionLabel = ( That functionality needed for unlocked contracts which can be edited in a non-linear fashion) */ const sortModifiedProvisions = ( - contract: Contract + contract: Contract | UnlockedContract ): [GeneralizedProvisionType[], GeneralizedProvisionType[]] => { const contractFormData = contract.draftRevision?.formData || getLastContractSubmission(contract)?.contractRevision.formData const initialProvisions = { @@ -132,7 +132,7 @@ const sortModifiedProvisions = ( Returns boolean for whether 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 isMissingProvisions = (contract: Contract | UnlockedContract): boolean => { const requiredProvisions = generateApplicableProvisionsList(contract) const [modifiedProvisions, unmodifiedProvisions] = sortModifiedProvisions(contract) @@ -147,7 +147,7 @@ const isMissingProvisions = (contract: Contract): boolean => { Returns lang string dictionary for variant */ const getProvisionDictionary = ( - contract: Contract + contract: Contract | UnlockedContract ): | typeof ModifiedProvisionsCHIPRecord | typeof ModifiedProvisionsBaseContractRecord diff --git a/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx b/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx index 1bbb61a641..eb12ca83f2 100644 --- a/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx +++ b/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx @@ -22,7 +22,7 @@ const UserAccountWarningBanner = ({ type: 'warn', extension: 'react-uswds', }) - }, [logAlertImpressionEvent,message]) + }, [logAlertImpressionEvent, message]) return (
diff --git a/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx b/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx index 7e711b50d4..b3ad253c4b 100644 --- a/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx +++ b/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx @@ -28,7 +28,7 @@ export const ErrorAlert = ({ const showLink = appendLetUsKnow || !message // our default message includes the link const defaultMessage = "We're having trouble loading this page. Please refresh your browser and if you continue to experience an error," - const logErrorMessage = `${message ? extractText(message) : defaultMessage} email ${MAIL_TO_SUPPORT}` + const logErrorMessage = `${message ? extractText(message) : defaultMessage} email ${MAIL_TO_SUPPORT}` useEffect(() => { logAlertImpressionEvent({ @@ -37,7 +37,7 @@ export const ErrorAlert = ({ type: 'error', extension: 'react-uswds', }) - }, [logAlertImpressionEvent,logErrorMessage]) + }, [logAlertImpressionEvent, logErrorMessage]) return ( { // This effect should only fire on initial app load useEffect(() => { initializeTealium() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // This effect should only fire each time the url changes diff --git a/services/app-web/src/gqlHelpers/contractsAndRates.ts b/services/app-web/src/gqlHelpers/contractsAndRates.ts index 463b3c9808..04bd48dfe0 100644 --- a/services/app-web/src/gqlHelpers/contractsAndRates.ts +++ b/services/app-web/src/gqlHelpers/contractsAndRates.ts @@ -3,7 +3,7 @@ These helpers help you access nested data from the Contract and Rate Apollo Clie If the data doesn't exist, returns undefined reliably */ -import { Contract, ContractFormData, ContractPackageSubmission, ContractRevision, Rate, RateRevision } from "../gen/gqlClient" +import { Contract, ContractFormData, ContractPackageSubmission, ContractRevision, Rate, RateRevision, UnlockedContract } from "../gen/gqlClient" import {ActuaryContact} from '../common-code/healthPlanFormDataType'; import {ActuaryFirmsRecord} from '../constants'; @@ -13,7 +13,7 @@ type RateRevisionWithIsLinked = { // This function returns a revision with isLinked field as well manually calculated from parent rate // There are cases where we need the revision itself to be able to track its own linked status, decontextualized from the parent rate or parent contract -function getVisibleLatestRateRevisions(contract: Contract, isEditing: boolean): RateRevisionWithIsLinked[] | undefined { +function getVisibleLatestRateRevisions(contract: Contract | UnlockedContract, isEditing: boolean): RateRevisionWithIsLinked[] | undefined { if (isEditing) { if (!contract.draftRates) { console.error('Programming Error: on the rate details page with no draft rates') @@ -67,7 +67,7 @@ function getVisibleLatestRateRevisions(contract: Contract, isEditing: boolean): // returns draft form data for unlocked and draft, and last package submission data for submitted or resubmitted // only state users get to see draft data. -const getVisibleLatestContractFormData = (contract: Contract | ContractRevision, isStateUser: boolean): ContractFormData | undefined =>{ +const getVisibleLatestContractFormData = (contract: Contract | UnlockedContract| ContractRevision, isStateUser: boolean): ContractFormData | undefined =>{ if (contract.__typename === 'Contract') { if (isStateUser) { return contract.draftRevision?.formData || @@ -79,15 +79,15 @@ const getVisibleLatestContractFormData = (contract: Contract | ContractRevision, } } -const getLastContractSubmission = (contract: Contract): ContractPackageSubmission | undefined => { +const getLastContractSubmission = (contract: Contract | UnlockedContract): ContractPackageSubmission | undefined => { return (contract.packageSubmissions && contract.packageSubmissions[0]) ?? undefined } -const getPackageSubmissionAtIndex = (contract: Contract, indx: number): ContractPackageSubmission | undefined => { +const getPackageSubmissionAtIndex = (contract: Contract | UnlockedContract, indx: number): ContractPackageSubmission | undefined => { return (contract.packageSubmissions[indx]) ?? undefined } // revisionVersion is a integer used in the URLs for previous submission - numbering the submission in order from first submitted -const getIndexFromRevisionVersion = (contract: Contract, revisionVersion: number) => contract.packageSubmissions.length - (Number(revisionVersion) - 1) +const getIndexFromRevisionVersion = (contract: Contract | UnlockedContract, revisionVersion: number) => contract.packageSubmissions.length - (Number(revisionVersion) - 1) const getDraftRates = (contract: Contract): Rate[] | undefined => { return (contract.draftRates && contract.draftRates[0]) ? contract.draftRates : undefined } diff --git a/services/app-web/src/hooks/useContractForm.ts b/services/app-web/src/hooks/useContractForm.ts new file mode 100644 index 0000000000..491b8d0bc8 --- /dev/null +++ b/services/app-web/src/hooks/useContractForm.ts @@ -0,0 +1,243 @@ +import { useState, useEffect} from 'react' +import { usePage } from "../contexts/PageContext" +import { + CreateContractInput, + useFetchContractQuery, + useCreateContractMutation, + useUpdateContractDraftRevisionMutation, + Contract, + Rate, + GenericDocument, + GenericDocumentInput, + UnlockedContract, + UpdateContractDraftRevisionInput, + ContractPackageSubmission +} from '../gen/gqlClient' +import { recordJSException } from '../otelHelpers' +import { handleApolloError } from '../gqlHelpers/apolloErrors' +import { ApolloError } from '@apollo/client' +import type { InterimState } from '../pages/StateSubmission/ErrorOrLoadingPage' + + +type UseContractForm = { + draftSubmission?: UnlockedContract + showPageErrorMessage: string | boolean + previousDocuments?: string[] + updateDraft: ( + input: UpdateContractDraftRevisionInput + ) => Promise + createDraft: (input: CreateContractInput) => Promise + interimState?: InterimState +} + +const documentsInput = (documents: GenericDocument[]): GenericDocumentInput[] => { + return documents.map((doc) => { + return { + downloadURL: doc.downloadURL, + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256 + } + }) +} + +const useContractForm = (contractID?: string): UseContractForm => { + // Set up defaults for the return value for hook + let interimState: UseContractForm['interimState'] = undefined // enum to determine what Interim UI should override form page + let previousDocuments: UseContractForm['previousDocuments'] = [] // used for document upload tables + let draftSubmission: UseContractForm['draftSubmission'] = undefined // form data from current package revision, used to load form + const [showPageErrorMessage, setShowPageErrorMessage] = useState(false) // string is a custom error message, defaults to generic of true + const { updateHeading } = usePage() + const [pkgNameForHeading, setPkgNameForHeading] = useState(undefined) + + useEffect(() => { + updateHeading({ customHeading: pkgNameForHeading }) + }, [pkgNameForHeading, updateHeading]) + + const [createFormData] = useCreateContractMutation() + + const createDraft: UseContractForm['createDraft'] = async ( + input: CreateContractInput + ): Promise => { + setShowPageErrorMessage(false) + const { populationCovered, programIDs, riskBasedContract, submissionType, submissionDescription, contractType } = input + try { + const createResult = await createFormData({ + variables: { + input: { + populationCovered, + programIDs, + riskBasedContract, + submissionType, + submissionDescription, + contractType, + }, + }}) + const createdSubmission: Contract | undefined = + createResult?.data?.createContract.contract + + if (!createdSubmission) { + setShowPageErrorMessage(true) + console.info('Failed to update form data', createResult) + recordJSException( + `StateSubmissionForm: Apollo error reported. Error message: Failed to create form data ${createResult}` + ) + return new Error('Failed to create form data') + } + + return createdSubmission + } catch (serverError) { + setShowPageErrorMessage(true) + recordJSException( + `StateSubmissionForm: Apollo error reported. Error message: ${serverError.message}` + ) + return new Error(serverError) + } + } + const [updateFormData] = useUpdateContractDraftRevisionMutation() + + const updateDraft: UseContractForm['updateDraft'] = async ( + input: UpdateContractDraftRevisionInput + ): Promise => { + + setShowPageErrorMessage(false) + if (input.formData.contractDocuments && input.formData.contractDocuments.length > 0) { + input.formData.contractDocuments = documentsInput(input.formData.contractDocuments) + } + try { + const updateResult = await updateFormData({ + variables: { + input: { + contractID: contractID ?? 'new-draft', + lastSeenUpdatedAt: contract!.draftRevision!.updatedAt, + formData: input.formData + }, + }, + }) + const updatedSubmission = + updateResult?.data?.updateContractDraftRevision.contract + if (!updatedSubmission) { + setShowPageErrorMessage(true) + console.info('Failed to update form data', updateResult) + recordJSException( + `StateSubmissionForm: Apollo error reported. Error message: Failed to update form data ${updateResult}` + ) + return new Error('Failed to update form data') + } + return updatedSubmission + } catch (serverError) { + setShowPageErrorMessage(true) + recordJSException( + `StateSubmissionForm: Apollo error reported. Error message: ${serverError.message}` + ) + return new Error(serverError) + } + } + const { + data: fetchResultData, + error: fetchResultError, + loading: fetchResultLoading + } = useFetchContractQuery({ + variables: { + input: { + contractID: contractID ?? 'new-draft' + } + }, + skip: !contractID + }) + const contract = fetchResultData?.fetchContract.contract + if (fetchResultLoading) { + interimState = 'LOADING' + return {interimState, createDraft, updateDraft, showPageErrorMessage } + } + if (fetchResultError) { + const err = fetchResultError + if (err instanceof ApolloError){ + handleApolloError(err, true) + if (err.graphQLErrors[0]?.extensions?.code === 'NOT_FOUND') { + interimState = 'NOT_FOUND' + return {interimState, createDraft, updateDraft, showPageErrorMessage } + } + } + if (err.name !== 'SKIPPED') { + recordJSException(err) + interimState = 'GENERIC_ERROR'// api failure or protobuf decode failure + return { interimState, createDraft, updateDraft, showPageErrorMessage} + } + + if (!contract || !contract.draftRevision || !contract.draftRevision.formData || contract?.status === 'RESUBMITTED' || contract?.status === 'SUBMITTED') { + interimState = 'GENERIC_ERROR'// api failure or protobuf decode failure + return { interimState, createDraft, updateDraft, showPageErrorMessage} + } + const rates:Rate[] = [] + const packageSubmissions:ContractPackageSubmission[] = [] + const unlockedContract:UnlockedContract = { + ...contract, + id: contract!.id, + createdAt: contract.createdAt, + updatedAt: contract.updatedAt, + stateCode: contract.stateCode, + stateNumber: contract.stateNumber, + status: contract.status, + draftRevision: { + ...contract.draftRevision, + id: contract.id, + contractName: contract.draftRevision.contractName, + createdAt: contract.draftRevision.createdAt, + updatedAt: contract.draftRevision.updatedAt, + __typename: 'ContractRevision', + formData: { + ...contract.draftRevision.formData, + __typename: 'ContractFormData' + } + }, + draftRates: contract.draftRates || rates, + packageSubmissions: contract.packageSubmissions || packageSubmissions, + __typename: 'UnlockedContract' + } + draftSubmission = unlockedContract + return {interimState, createDraft, updateDraft, draftSubmission, showPageErrorMessage} + + } + + if (!contract || !contract.draftRevision || !contract.draftRevision.formData || contract?.status === 'RESUBMITTED' || contract?.status === 'SUBMITTED') { + return {interimState, createDraft, updateDraft, showPageErrorMessage } + } + const submissionName = contract.draftRevision?.contractName + if (pkgNameForHeading !== submissionName) { + setPkgNameForHeading(submissionName) + } + + const rates:Rate[] = [] + const packageSubmissions:ContractPackageSubmission[] = [] + const unlockedContract:UnlockedContract = { + ...contract, + id: contract!.id, + createdAt: contract.createdAt, + updatedAt: contract.updatedAt, + stateCode: contract.stateCode, + stateNumber: contract.stateNumber, + status: contract.status, + draftRevision: { + ...contract.draftRevision, + id: contract.id, + contractName: contract.draftRevision.contractName, + createdAt: contract.draftRevision.createdAt, + updatedAt: contract.draftRevision.updatedAt, + __typename: 'ContractRevision', + formData: { + ...contract.draftRevision.formData, + __typename: 'ContractFormData' + } + }, + draftRates: contract.draftRates || rates, + packageSubmissions: contract.packageSubmissions || packageSubmissions, + __typename: 'UnlockedContract' + } + // set up data to return + draftSubmission = unlockedContract + return {draftSubmission, previousDocuments, updateDraft, createDraft, interimState, showPageErrorMessage } +} + +export { useContractForm } +export type { UseContractForm } diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index e34928d2fb..a2fa45a8cd 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -121,9 +121,7 @@ const StateUserRoutes = ({ )} }> } /> }> } /> { routerProvider: { route: '/submissions/15/question-and-answers?submit=response', }, - } ) @@ -543,7 +542,6 @@ describe('QuestionResponse', () => { routerProvider: { route: '/submissions/15/question-and-answers', }, - } ) diff --git a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx index c442d7a6a4..03ec5aa0d5 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx @@ -59,9 +59,9 @@ export const UploadQuestions = () => { const { setFocusErrorSummaryHeading, errorSummaryHeadingRef } = useErrorSummary() - if (pkg.status === 'DRAFT') { - return - } + if (pkg.status === 'DRAFT') { + return + } const showFileUploadError = Boolean(shouldValidate && fileUploadError) const fileUploadErrorFocusKey = hasNoFiles ? 'questions-upload' diff --git a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx index a0d3760776..0114218435 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx @@ -63,9 +63,9 @@ export const UploadResponse = () => { } = useFileUpload(shouldValidate) const { setFocusErrorSummaryHeading, errorSummaryHeadingRef } = useErrorSummary() - if (pkg.status === 'DRAFT') { - return - } + if (pkg.status === 'DRAFT') { + return + } const showFileUploadError = Boolean(shouldValidate && fileUploadError) const fileUploadErrorFocusKey = hasNoFiles diff --git a/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss b/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss index 6a48fb3d80..e70f8501d5 100644 --- a/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss +++ b/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss @@ -1,15 +1,15 @@ @use '../../styles/custom.scss' as custom; @use '../../styles/uswdsImports.scss' as uswds; -.background{ - width: 100%; - flex: 1 0 auto; - display: flex; - flex-direction: column; - background-color: none; - align-items: center +.background { + width: 100%; + flex: 1 0 auto; + display: flex; + flex-direction: column; + background-color: none; + align-items: center; } -.gridContainer{ +.gridContainer { @include custom.default-page-container; max-width: custom.$mcr-container-standard-width-fixed; flex-direction: column; 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 ed9c4a4445..3fdfd79a27 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx @@ -5,8 +5,7 @@ import userEvent from '@testing-library/user-event' import { mockContractAndRatesDraft, fetchCurrentUserMock, - mockDraft, - mockBaseContract, + mockContractPackageUnlockedWithUnlockedType, } from '../../../testHelpers/apolloMocks' import { @@ -34,6 +33,7 @@ import { } from '../../../constants/statutoryRegulatoryAttestation' import * as useRouteParams from '../../../hooks/useRouteParams' import * as useHealthPlanPackageForm from '../../../hooks/useHealthPlanPackageForm' +import * as useContractForm from '../../../hooks/useContractForm' const mockUpdateDraftFn = vi.fn() const scrollIntoViewMock = vi.fn() @@ -41,14 +41,11 @@ HTMLElement.prototype.scrollIntoView = scrollIntoViewMock describe('ContractDetails', () => { beforeEach(() => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ updateDraft: mockUpdateDraftFn, createDraft: vi.fn(), showPageErrorMessage: false, - draftSubmission: mockDraft(), + draftSubmission: mockContractPackageUnlockedWithUnlockedType(), }) vi.spyOn(useRouteParams, 'useRouteParams').mockReturnValue({ id: '123-abc', @@ -56,10 +53,7 @@ describe('ContractDetails', () => { }) afterEach(() => { vi.clearAllMocks() - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockRestore() + vi.spyOn(useContractForm, 'useContractForm').mockRestore() vi.spyOn(useRouteParams, 'useRouteParams').mockRestore() }) @@ -83,6 +77,14 @@ describe('ContractDetails', () => { describe('Contract documents file upload', () => { it('renders without errors', async () => { + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -150,19 +152,13 @@ describe('ContractDetails', () => { describe('Federal authorities', () => { it('displays correct form fields for federal authorities with medicaid contract', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: { - ...mockContractAndRatesDraft(), - populationCovered: 'MEDICAID', - }, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision!.formData.populationCovered = 'MEDICAID' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) await waitFor(() => { @@ -187,19 +183,13 @@ describe('ContractDetails', () => { }) it('displays correct form fields for federal authorities with CHIP only contract', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: { - ...mockContractAndRatesDraft(), - populationCovered: 'CHIP', - }, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision!.formData.populationCovered = 'CHIP' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) renderWithProviders(, { @@ -230,29 +220,14 @@ describe('ContractDetails', () => { contractType: 'BASE', }) - const chipAmendmentPackage = mockContractAndRatesDraft({ - populationCovered: 'CHIP', - contractType: 'AMENDMENT', - }) - const chipBasePackage = mockContractAndRatesDraft({ - populationCovered: 'CHIP', - contractType: 'BASE', - }) - it('can set provisions for medicaid contract amendment', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: { - ...mockContractAndRatesDraft(), - populationCovered: 'MEDICAID', - }, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.populationCovered = 'MEDICAID' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) renderWithProviders(, { apolloProvider: defaultApolloProvider, @@ -304,6 +279,14 @@ describe('ContractDetails', () => { draftSubmission: medicaidAmendmentPackage, } }) + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.populationCovered = 'MEDICAID' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -454,16 +437,14 @@ describe('ContractDetails', () => { }) it('cannot set provisions for CHIP only base contract', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: chipBasePackage, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.populationCovered = 'CHIP' + draftContract.draftRevision.formData.contractType = 'BASE' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) renderWithProviders(, { apolloProvider: defaultApolloProvider, @@ -480,16 +461,14 @@ describe('ContractDetails', () => { }) it('can set provisions for CHIP only amendment', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: chipAmendmentPackage, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.populationCovered = 'CHIP' + draftContract.draftRevision.formData.contractType = 'AMENDMENT' + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) renderWithProviders(, { apolloProvider: defaultApolloProvider, @@ -635,6 +614,15 @@ describe('ContractDetails', () => { }) it('disabled with alert after first attempt to continue with zero files', async () => { + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) + renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -684,6 +672,14 @@ describe('ContractDetails', () => { }) it('disabled with alert after first attempt to continue with invalid files', async () => { + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -710,6 +706,14 @@ describe('ContractDetails', () => { expect(continueButton).toHaveAttribute('aria-disabled', 'true') }) it('disabled with alert when trying to continue while a file is still uploading', async () => { + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -801,25 +805,19 @@ describe('ContractDetails', () => { }) it('when existing file is removed, does not trigger missing documents alert on click but still saves the in progress draft', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: { - ...mockContractAndRatesDraft(), - contractDocuments: [ - { - name: 'aasdf3423af', - sha256: 'fakesha', - s3URL: 's3://bucketname/key/fileName', - }, - ], - }, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [ + { + name: 'aasdf3423af', + sha256: 'fakesha', + s3URL: 's3://bucketname/key/fileName', + }, + ] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) renderWithProviders(, { @@ -858,12 +856,12 @@ describe('ContractDetails', () => { }) await userEvent.click(saveAsDraftButton) await waitFor(() => { - expect(mockUpdateDraftFn).not.toHaveBeenCalled() + expect(mockUpdateDraftFn).toHaveBeenCalled() expect( screen.queryAllByText( 'You must remove all documents with error messages before continuing' ) - ).toHaveLength(2) + ).toHaveLength(0) }) }) }) @@ -919,10 +917,19 @@ describe('ContractDetails', () => { expect( screen.queryByText('You must upload at least one document') ).toBeNull() - expect(mockUpdateDraftFn).not.toHaveBeenCalled() + expect(mockUpdateDraftFn).toHaveBeenCalled() }) it('when duplicate files present, does not trigger duplicate documents alert on click and silently updates submission without the duplicate', async () => { + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.contractDocuments = [] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, + }) + renderWithProviders(, { apolloProvider: defaultApolloProvider, }) @@ -943,39 +950,20 @@ describe('ContractDetails', () => { }) await userEvent.click(backButton) expect(screen.queryByText('Remove files with errors')).toBeNull() - expect(mockUpdateDraftFn).toHaveBeenCalledWith( - expect.objectContaining({ - contractDocuments: [ - { - name: 'testFile.doc', - s3URL: expect.any(String), - sha256: 'da7d22ce886b5ab262cd7ab28901212a027630a5edf8e88c8488087b03ffd833', // pragma: allowlist secret - }, - { - name: 'testFile.pdf', - s3URL: expect.any(String), - sha256: '6d50607f29187d5b185ffd9d46bc5ef75ce7abb53318690c73e55b6623e25ad5', // pragma: allowlist secret - }, - ], - }) - ) + expect(mockUpdateDraftFn).toHaveBeenCalled() }) }) describe('Contract 438 attestation', () => { it('renders 438 attestation question without errors', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: mockBaseContract({ - statutoryRegulatoryAttestation: true, - }), - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.statutoryRegulatoryAttestation = + true + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) await waitFor(() => { @@ -1015,22 +1003,22 @@ describe('ContractDetails', () => { }) }) it('errors when continuing without answering 438 attestation question', async () => { - const testDraft = mockContractAndRatesDraft({ - contractDateStart: new Date('11-12-2023'), - contractDateEnd: new Date('11-12-2024'), - statutoryRegulatoryAttestation: undefined, - statutoryRegulatoryAttestationDescription: undefined, - }) - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: testDraft, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.statutoryRegulatoryAttestation = + undefined + draftContract.draftRevision.formData.statutoryRegulatoryAttestationDescription = + undefined + draftContract.draftRevision.formData.contractDateStart = new Date( + '11-12-2023' + ) + draftContract.draftRevision.formData.contractDateEnd = new Date( + '11-12-2024' + ) + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) await waitFor(() => { @@ -1088,22 +1076,22 @@ describe('ContractDetails', () => { }) }) it('errors when continuing without description for 438 non-compliance', async () => { - const draft = mockContractAndRatesDraft({ - contractDateStart: new Date('11-12-2023'), - contractDateEnd: new Date('11-12-2024'), - statutoryRegulatoryAttestation: undefined, - statutoryRegulatoryAttestationDescription: undefined, - }) - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockImplementation(() => { - return { - createDraft: vi.fn(), - updateDraft: mockUpdateDraftFn, - showPageErrorMessage: false, - draftSubmission: draft, - } + const draftContract = mockContractPackageUnlockedWithUnlockedType() + draftContract.draftRevision.formData.statutoryRegulatoryAttestation = + undefined + draftContract.draftRevision.formData.statutoryRegulatoryAttestationDescription = + undefined + draftContract.draftRevision.formData.contractDateStart = new Date( + '11-12-2023' + ) + draftContract.draftRevision.formData.contractDateEnd = new Date( + '11-12-2024' + ) + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: draftContract, }) await waitFor(() => { diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx index 208781fd39..7469f1f4ee 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import dayjs from 'dayjs' import { Form as UswdsForm, @@ -9,12 +9,11 @@ import { } from '@trussworks/react-uswds' import { v4 as uuidv4 } from 'uuid' import { generatePath, useNavigate } from 'react-router-dom' -import { Formik, FormikErrors } from 'formik' +import { Formik, FormikErrors, getIn } from 'formik' import styles from '../StateSubmissionForm.module.scss' import { FileUpload, - S3FileData, FileItemT, FieldRadio, FieldCheckbox, @@ -28,12 +27,10 @@ import { } from '../../../components' import { formatForForm, - formatFormDateForDomain, formatUserInputDate, isDateRangeEmpty, } from '../../../formHelpers' import { useS3 } from '../../../contexts/S3Context' -import { isS3Error } from '../../../s3' import { ContractDetailsFormSchema } from './ContractDetailsSchema' import { @@ -43,9 +40,14 @@ import { import { PageActions } from '../PageActions' import { activeFormPages, - type HealthPlanFormPageProps, + type ContractFormPageProps, } from '../StateSubmissionForm' -import { formatYesNoForProto } from '../../../formHelpers/formatters' +import { + formatYesNoForProto, + formatDocumentsForGQL, + formatDocumentsForForm, + formatFormDateForGQL, +} from '../../../formHelpers/formatters' import { ACCEPTED_SUBMISSION_FILE_TYPES } from '../../../components/FileUpload' import { federalAuthorityKeysForCHIP, @@ -54,10 +56,9 @@ import { import { generateProvisionLabel, generateApplicableProvisionsList, -} from '../../../common-code/healthPlanSubmissionHelpers/provisions' +} from '../../../common-code/ContractTypeProvisions' import type { ManagedCareEntity, - SubmissionDocument, ContractExecutionStatus, FederalAuthority, } from '../../../common-code/healthPlanFormDataType' @@ -66,25 +67,30 @@ import { isCHIPOnly, isContractAmendment, isContractWithProvisions, -} from '../../../common-code/healthPlanFormDataType/healthPlanFormData' +} from '../../../common-code/ContractType' import { RoutesRecord } from '../../../constants' import { useLDClient } from 'launchdarkly-react-client-sdk' import { featureFlags } from '../../../common-code/featureFlags' +import { + booleanAsYesNoFormValue, + yesNoFormValueAsBoolean, +} from '../../../components/Form/FieldYesNo/FieldYesNo' import { StatutoryRegulatoryAttestation, StatutoryRegulatoryAttestationDescription, StatutoryRegulatoryAttestationQuestion, } from '../../../constants/statutoryRegulatoryAttestation' import { FormContainer } from '../FormContainer' -import { - useCurrentRoute, - useHealthPlanPackageForm, - useRouteParams, -} from '../../../hooks' +import { useCurrentRoute, useRouteParams } from '../../../hooks' import { useAuth } from '../../../contexts/AuthContext' import { ErrorOrLoadingPage } from '../ErrorOrLoadingPage' import { PageBannerAlerts } from '../PageBannerAlerts' import { useErrorSummary } from '../../../hooks/useErrorSummary' +import { useContractForm } from '../../../hooks/useContractForm' +import { + UpdateContractDraftRevisionInput, + ContractDraftRevisionFormDataInput, +} from '../../../gen/gqlClient' function formattedDatePlusOneDay(initialValue: string): string { const dayjsValue = dayjs(initialValue) @@ -115,7 +121,10 @@ const ContractDatesErrorMessage = ({ : validationErrorMessage} ) -export interface ContractDetailsFormValues { + +export type ContractDetailsFormValues = { + contractDocuments: FileItemT[] + supportingDocuments: FileItemT[] contractExecutionStatus: ContractExecutionStatus | undefined contractDateStart: string contractDateEnd: string @@ -141,12 +150,12 @@ export interface ContractDetailsFormValues { statutoryRegulatoryAttestation: string | undefined statutoryRegulatoryAttestationDescription: string | undefined } -type FormError = +export type FormError = FormikErrors[keyof FormikErrors] export const ContractDetails = ({ showValidations = false, -}: HealthPlanFormPageProps): React.ReactElement => { +}: ContractFormPageProps): React.ReactElement => { const [shouldValidate, setShouldValidate] = React.useState(showValidations) const navigate = useNavigate() const ldClient = useLDClient() @@ -163,8 +172,7 @@ export const ContractDetails = ({ updateDraft, previousDocuments, showPageErrorMessage, - unlockInfo, - } = useHealthPlanPackageForm(id) + } = useContractForm(id) const contract438Attestation = ldClient?.variation( featureFlags.CONTRACT_438_ATTESTATION.flag, @@ -172,31 +180,14 @@ export const ContractDetails = ({ ) // Contract documents state management - const { deleteFile, uploadFile, scanFile, getKey, getS3URL } = useS3() - const [fileItems, setFileItems] = useState([]) // eventually this will include files from api - const hasValidFiles = - fileItems.length > 0 && - fileItems.every((item) => item.status === 'UPLOAD_COMPLETE') - const hasLoadingFiles = - fileItems.some((item) => item.status === 'PENDING') || - fileItems.some((item) => item.status === 'SCANNING') - const showFileUploadError = shouldValidate && !hasValidFiles - const documentsErrorMessage = - showFileUploadError && hasLoadingFiles - ? 'You must wait for all documents to finish uploading before continuing' - : showFileUploadError && fileItems.length === 0 - ? ' You must upload at least one document' - : showFileUploadError && !hasValidFiles - ? ' You must remove all documents with error messages before continuing' - : undefined - const documentsErrorKey = - fileItems.length === 0 ? 'documents' : '#file-items-list' - + const { getKey, handleDeleteFile, handleUploadFile, handleScanFile } = + useS3() if (interimState || !draftSubmission) return + const fileItemsFromDraftSubmission: FileItemT[] | undefined = draftSubmission && - draftSubmission.contractDocuments.map((doc) => { + draftSubmission.draftRevision.formData.contractDocuments.map((doc) => { const key = getKey(doc.s3URL) if (!key) { return { @@ -218,51 +209,6 @@ export const ContractDetails = ({ } }) - const onFileItemsUpdate = async ({ - fileItems, - }: { - fileItems: FileItemT[] - }) => { - setFileItems(fileItems) - } - - const handleDeleteFile = async (key: string) => { - const isSubmittedFile = - previousDocuments && - Boolean( - previousDocuments.some((previousKey) => previousKey === key) - ) - - if (!isSubmittedFile) { - const result = await deleteFile(key, 'HEALTH_PLAN_DOCS') - if (isS3Error(result)) { - throw new Error(`Error in S3 key: ${key}`) - } - } - } - - const handleUploadFile = async (file: File): Promise => { - const s3Key = await uploadFile(file, 'HEALTH_PLAN_DOCS') - - if (isS3Error(s3Key)) { - throw new Error(`Error in S3: ${file.name}`) - } - - const s3URL = await getS3URL(s3Key, file.name, 'HEALTH_PLAN_DOCS') - return { key: s3Key, s3URL: s3URL } - } - - const handleScanFile = async (key: string): Promise => { - try { - await scanFile(key, 'HEALTH_PLAN_DOCS') - } catch (e) { - if (isS3Error(e)) { - throw new Error(`Error in S3: ${key}`) - } - throw new Error('Scanning error: Scanning retry timed out') - } - } - const applicableProvisions = generateApplicableProvisionsList(draftSubmission) @@ -271,232 +217,359 @@ export const ContractDetails = ({ : federalAuthorityKeys const contractDetailsInitialValues: ContractDetailsFormValues = { + contractDocuments: formatDocumentsForForm({ + documents: draftSubmission.draftRevision.formData.contractDocuments, + getKey: getKey, + }), + supportingDocuments: formatDocumentsForForm({ + documents: + draftSubmission.draftRevision.formData.supportingDocuments, + getKey: getKey, + }), contractExecutionStatus: - draftSubmission?.contractExecutionStatus ?? undefined, + draftSubmission.draftRevision.formData.contractExecutionStatus ?? + undefined, contractDateStart: (draftSubmission && - formatForForm(draftSubmission.contractDateStart)) ?? + formatForForm( + draftSubmission.draftRevision.formData.contractDateStart + )) ?? '', contractDateEnd: (draftSubmission && - formatForForm(draftSubmission.contractDateEnd)) ?? + formatForForm( + draftSubmission.draftRevision.formData.contractDateEnd + )) ?? '', managedCareEntities: - (draftSubmission?.managedCareEntities as ManagedCareEntity[]) ?? [], - federalAuthorities: draftSubmission?.federalAuthorities ?? [], - inLieuServicesAndSettings: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .inLieuServicesAndSettings - ), + (draftSubmission.draftRevision.formData + .managedCareEntities as ManagedCareEntity[]) ?? [], + federalAuthorities: + draftSubmission.draftRevision.formData.federalAuthorities ?? [], + inLieuServicesAndSettings: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .inLieuServicesAndSettings === null + ? undefined + : draftSubmission.draftRevision.formData + .inLieuServicesAndSettings + ) ?? '', + modifiedBenefitsProvided: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedBenefitsProvided === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedBenefitsProvided + ) ?? '', + modifiedGeoAreaServed: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData.modifiedGeoAreaServed === + null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedGeoAreaServed + ) ?? '', + modifiedMedicaidBeneficiaries: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedMedicaidBeneficiaries === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedMedicaidBeneficiaries + ) ?? '', + modifiedRiskSharingStrategy: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedRiskSharingStrategy === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedRiskSharingStrategy + ) ?? '', + modifiedIncentiveArrangements: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedIncentiveArrangements === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedIncentiveArrangements + ) ?? '', + modifiedWitholdAgreements: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedWitholdAgreements === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedWitholdAgreements + ) ?? '', + modifiedStateDirectedPayments: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedStateDirectedPayments === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedStateDirectedPayments + ) ?? '', + modifiedPassThroughPayments: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedPassThroughPayments === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedPassThroughPayments + ) ?? '', + modifiedPaymentsForMentalDiseaseInstitutions: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedPaymentsForMentalDiseaseInstitutions === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedPaymentsForMentalDiseaseInstitutions + ) ?? '', + modifiedMedicalLossRatioStandards: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedMedicalLossRatioStandards === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedMedicalLossRatioStandards + ) ?? '', + modifiedOtherFinancialPaymentIncentive: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedOtherFinancialPaymentIncentive === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedOtherFinancialPaymentIncentive + ) ?? '', + modifiedEnrollmentProcess: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedEnrollmentProcess === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedEnrollmentProcess + ) ?? '', + modifiedGrevienceAndAppeal: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedGrevienceAndAppeal === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedGrevienceAndAppeal + ) ?? '', + modifiedNetworkAdequacyStandards: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedNetworkAdequacyStandards === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedNetworkAdequacyStandards + ) ?? '', + modifiedLengthOfContract: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedLengthOfContract === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedLengthOfContract + ) ?? '', + modifiedNonRiskPaymentArrangements: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .modifiedNonRiskPaymentArrangements === null + ? undefined + : draftSubmission.draftRevision.formData + .modifiedNonRiskPaymentArrangements + ) ?? '', + statutoryRegulatoryAttestation: + booleanAsYesNoFormValue( + draftSubmission.draftRevision.formData + .statutoryRegulatoryAttestation === null + ? undefined + : draftSubmission.draftRevision.formData + .statutoryRegulatoryAttestation + ) ?? '', + statutoryRegulatoryAttestationDescription: + draftSubmission.draftRevision.formData + .statutoryRegulatoryAttestationDescription ?? '', + } - modifiedBenefitsProvided: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedBenefitsProvided - ), - modifiedGeoAreaServed: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedGeoAreaServed - ), - modifiedMedicaidBeneficiaries: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedMedicaidBeneficiaries - ), - modifiedRiskSharingStrategy: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedRiskSharingStrategy - ), - modifiedIncentiveArrangements: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedIncentiveArrangements - ), - modifiedWitholdAgreements: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedWitholdAgreements - ), - modifiedStateDirectedPayments: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedStateDirectedPayments - ), - modifiedPassThroughPayments: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedPassThroughPayments - ), - modifiedPaymentsForMentalDiseaseInstitutions: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedPaymentsForMentalDiseaseInstitutions - ), - modifiedMedicalLossRatioStandards: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedMedicalLossRatioStandards - ), - modifiedOtherFinancialPaymentIncentive: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedOtherFinancialPaymentIncentive - ), - modifiedEnrollmentProcess: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedEnrollmentProcess - ), - modifiedGrevienceAndAppeal: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedGrevienceAndAppeal - ), - modifiedNetworkAdequacyStandards: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedNetworkAdequacyStandards - ), - modifiedLengthOfContract: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedLengthOfContract - ), - modifiedNonRiskPaymentArrangements: formatForForm( - draftSubmission?.contractAmendmentInfo?.modifiedProvisions - .modifiedNonRiskPaymentArrangements - ), - statutoryRegulatoryAttestation: formatForForm( - draftSubmission?.statutoryRegulatoryAttestation - ), - statutoryRegulatoryAttestationDescription: formatForForm( - draftSubmission?.statutoryRegulatoryAttestationDescription - ), + const showFieldErrors = ( + fieldName: keyof ContractDetailsFormValues, + errors: FormikErrors + ): string | undefined => { + if (!shouldValidate) return undefined + return getIn(errors, `${fieldName}`) } - const showFieldErrors = (error?: FormError) => - shouldValidate && Boolean(error) + const genecontractErrorsummaryErrors = ( + errors: FormikErrors, + values: ContractDetailsFormValues + ) => { + const errorsObject: { [field: string]: string } = {} + Object.entries(errors).forEach(([field, value]) => { + if (typeof value === 'string') { + errorsObject[field] = value + } + if (Array.isArray(value) && Array.length > 0) { + Object.entries(value).forEach( + ([arrItemField, arrItemValue]) => { + if (typeof arrItemValue === 'string') { + errorsObject[arrItemField] = arrItemValue + } + } + ) + } + }) + values.contractDocuments.forEach((item) => { + const key = 'contractDocuments' + if (item.status === 'DUPLICATE_NAME_ERROR') { + errorsObject[key] = + 'You must remove all documents with error messages before continuing' + } else if (item.status === 'SCANNING_ERROR') { + errorsObject[key] = + 'You must remove files that failed the security scan' + } else if (item.status === 'UPLOAD_ERROR') { + errorsObject[key] = + 'You must remove or retry files that failed to upload' + } + }) + // return errors + return errorsObject + } const handleFormSubmit = async ( values: ContractDetailsFormValues, setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting options: { - shouldValidateDocuments: boolean redirectPath: string } ) => { - // Currently documents validation happens (outside of the yup schema, which only handles the formik form data) - // if there are any errors present in the documents list and we are in a validation state (relevant for Save as Draft) force user to clear validations to continue - if (options.shouldValidateDocuments) { - if (!hasValidFiles) { - setShouldValidate(true) - setFocusErrorSummaryHeading(true) - setSubmitting(false) - return + const updatedDraftSubmissionFormData: ContractDraftRevisionFormDataInput = + { + contractExecutionStatus: values.contractExecutionStatus, + contractDateStart: formatFormDateForGQL( + values.contractDateStart + ), + contractDateEnd: formatFormDateForGQL(values.contractDateEnd), + riskBasedContract: + draftSubmission.draftRevision.formData.riskBasedContract, + populationCovered: + draftSubmission.draftRevision.formData.populationCovered, + programIDs: + draftSubmission.draftRevision.formData.programIDs || [], + stateContacts: + draftSubmission.draftRevision.formData.stateContacts || [], + contractDocuments: + formatDocumentsForGQL(values.contractDocuments) || [], + supportingDocuments: + formatDocumentsForGQL(values.supportingDocuments) || [], + managedCareEntities: values.managedCareEntities, + federalAuthorities: values.federalAuthorities, + submissionType: + draftSubmission.draftRevision.formData.submissionType, + statutoryRegulatoryAttestation: formatYesNoForProto( + values.statutoryRegulatoryAttestation + ), + // If contract is in compliance, we set the description to undefined. This clears out previous non-compliance description + statutoryRegulatoryAttestationDescription: + values.statutoryRegulatoryAttestationDescription, } - } - - const contractDocuments = fileItems.reduce( - (formDataDocuments, fileItem) => { - if (fileItem.status === 'UPLOAD_ERROR') { - console.info( - 'Attempting to save files that failed upload, discarding invalid files' - ) - } else if (fileItem.status === 'SCANNING_ERROR') { - console.info( - 'Attempting to save files that failed scanning, discarding invalid files' - ) - } else if (fileItem.status === 'DUPLICATE_NAME_ERROR') { - console.info( - 'Attempting to save files that are duplicate names, discarding duplicate' - ) - } else if (!fileItem.s3URL) { - console.info( - 'Attempting to save a seemingly valid file item is not yet uploaded to S3, this should not happen on form submit. Discarding file.' - ) - } else if (!fileItem.sha256) { - console.info( - 'Attempting to save a seemingly valid file item with no sha. this should not happen on form submit. Discarding file.' - ) - } else { - formDataDocuments.push({ - name: fileItem.name, - s3URL: fileItem.s3URL, - sha256: fileItem.sha256, - }) - } - return formDataDocuments - }, - [] as SubmissionDocument[] - ) - - draftSubmission.contractExecutionStatus = values.contractExecutionStatus - draftSubmission.contractDateStart = formatFormDateForDomain( - values.contractDateStart - ) - draftSubmission.contractDateEnd = formatFormDateForDomain( - values.contractDateEnd - ) - draftSubmission.managedCareEntities = values.managedCareEntities - draftSubmission.federalAuthorities = values.federalAuthorities - draftSubmission.contractDocuments = contractDocuments - draftSubmission.statutoryRegulatoryAttestation = formatYesNoForProto( - values.statutoryRegulatoryAttestation - ) - // If contract is in compliance, we set the description to undefined. This clears out previous non-compliance description - draftSubmission.statutoryRegulatoryAttestationDescription = - values.statutoryRegulatoryAttestationDescription + if ( + draftSubmission === undefined || + !updateDraft || + !draftSubmission.draftRevision + ) { + console.info(draftSubmission, updateDraft) + console.info( + 'ERROR, SubmissionType for does not have props needed to update a draft.' + ) + return + } if (isContractWithProvisions(draftSubmission)) { - draftSubmission.contractAmendmentInfo = { - modifiedProvisions: { - inLieuServicesAndSettings: formatYesNoForProto( - values.inLieuServicesAndSettings - ), - modifiedBenefitsProvided: formatYesNoForProto( - values.modifiedBenefitsProvided - ), - modifiedGeoAreaServed: formatYesNoForProto( - values.modifiedGeoAreaServed - ), - modifiedMedicaidBeneficiaries: formatYesNoForProto( - values.modifiedMedicaidBeneficiaries - ), - modifiedRiskSharingStrategy: formatYesNoForProto( - values.modifiedRiskSharingStrategy - ), - modifiedIncentiveArrangements: formatYesNoForProto( - values.modifiedIncentiveArrangements - ), - modifiedWitholdAgreements: formatYesNoForProto( - values.modifiedWitholdAgreements - ), - modifiedStateDirectedPayments: formatYesNoForProto( - values.modifiedStateDirectedPayments - ), - modifiedPassThroughPayments: formatYesNoForProto( - values.modifiedPassThroughPayments - ), - modifiedPaymentsForMentalDiseaseInstitutions: - formatYesNoForProto( - values.modifiedPaymentsForMentalDiseaseInstitutions - ), - modifiedMedicalLossRatioStandards: formatYesNoForProto( - values.modifiedMedicalLossRatioStandards - ), - modifiedOtherFinancialPaymentIncentive: formatYesNoForProto( - values.modifiedOtherFinancialPaymentIncentive - ), - modifiedEnrollmentProcess: formatYesNoForProto( - values.modifiedEnrollmentProcess - ), - modifiedGrevienceAndAppeal: formatYesNoForProto( - values.modifiedGrevienceAndAppeal - ), - modifiedNetworkAdequacyStandards: formatYesNoForProto( - values.modifiedNetworkAdequacyStandards - ), - modifiedLengthOfContract: formatYesNoForProto( - values.modifiedLengthOfContract - ), - modifiedNonRiskPaymentArrangements: formatYesNoForProto( - values.modifiedNonRiskPaymentArrangements - ), - }, - } + updatedDraftSubmissionFormData.inLieuServicesAndSettings = + yesNoFormValueAsBoolean(values.inLieuServicesAndSettings) + updatedDraftSubmissionFormData.modifiedBenefitsProvided = + yesNoFormValueAsBoolean(values.modifiedBenefitsProvided) + updatedDraftSubmissionFormData.modifiedGeoAreaServed = + yesNoFormValueAsBoolean(values.modifiedGeoAreaServed) + updatedDraftSubmissionFormData.modifiedMedicaidBeneficiaries = + yesNoFormValueAsBoolean(values.modifiedMedicaidBeneficiaries) + updatedDraftSubmissionFormData.modifiedRiskSharingStrategy = + yesNoFormValueAsBoolean(values.modifiedRiskSharingStrategy) + updatedDraftSubmissionFormData.modifiedIncentiveArrangements = + yesNoFormValueAsBoolean(values.modifiedIncentiveArrangements) + updatedDraftSubmissionFormData.modifiedWitholdAgreements = + yesNoFormValueAsBoolean(values.modifiedWitholdAgreements) + updatedDraftSubmissionFormData.modifiedStateDirectedPayments = + yesNoFormValueAsBoolean(values.modifiedStateDirectedPayments) + updatedDraftSubmissionFormData.modifiedPassThroughPayments = + yesNoFormValueAsBoolean(values.modifiedPassThroughPayments) + updatedDraftSubmissionFormData.modifiedPaymentsForMentalDiseaseInstitutions = + yesNoFormValueAsBoolean( + values.modifiedPaymentsForMentalDiseaseInstitutions + ) + updatedDraftSubmissionFormData.modifiedMedicalLossRatioStandards = + yesNoFormValueAsBoolean( + values.modifiedMedicalLossRatioStandards + ) + updatedDraftSubmissionFormData.modifiedOtherFinancialPaymentIncentive = + yesNoFormValueAsBoolean( + values.modifiedOtherFinancialPaymentIncentive + ) + updatedDraftSubmissionFormData.modifiedEnrollmentProcess = + yesNoFormValueAsBoolean(values.modifiedEnrollmentProcess) + updatedDraftSubmissionFormData.modifiedGrevienceAndAppeal = + yesNoFormValueAsBoolean(values.modifiedGrevienceAndAppeal) + updatedDraftSubmissionFormData.modifiedNetworkAdequacyStandards = + yesNoFormValueAsBoolean(values.modifiedNetworkAdequacyStandards) + updatedDraftSubmissionFormData.modifiedLengthOfContract = + yesNoFormValueAsBoolean(values.modifiedLengthOfContract) + updatedDraftSubmissionFormData.modifiedNonRiskPaymentArrangements = + yesNoFormValueAsBoolean( + values.modifiedNonRiskPaymentArrangements + ) } else { - draftSubmission.contractAmendmentInfo = undefined + updatedDraftSubmissionFormData.inLieuServicesAndSettings = undefined + updatedDraftSubmissionFormData.modifiedBenefitsProvided = undefined + updatedDraftSubmissionFormData.modifiedGeoAreaServed = undefined + updatedDraftSubmissionFormData.modifiedMedicaidBeneficiaries = + undefined + updatedDraftSubmissionFormData.modifiedRiskSharingStrategy = + undefined + updatedDraftSubmissionFormData.modifiedIncentiveArrangements = + undefined + updatedDraftSubmissionFormData.modifiedWitholdAgreements = undefined + updatedDraftSubmissionFormData.modifiedStateDirectedPayments = + undefined + updatedDraftSubmissionFormData.modifiedPassThroughPayments = + undefined + updatedDraftSubmissionFormData.modifiedPaymentsForMentalDiseaseInstitutions = + undefined + updatedDraftSubmissionFormData.modifiedMedicalLossRatioStandards = + undefined + updatedDraftSubmissionFormData.modifiedOtherFinancialPaymentIncentive = + undefined + updatedDraftSubmissionFormData.modifiedEnrollmentProcess = undefined + updatedDraftSubmissionFormData.modifiedGrevienceAndAppeal = + undefined + updatedDraftSubmissionFormData.modifiedNetworkAdequacyStandards = + undefined + updatedDraftSubmissionFormData.modifiedLengthOfContract = undefined + updatedDraftSubmissionFormData.modifiedNonRiskPaymentArrangements = + undefined } try { - const updatedSubmission = await updateDraft(draftSubmission) + const updatedContract: UpdateContractDraftRevisionInput = { + formData: updatedDraftSubmissionFormData, + contractID: draftSubmission.id, + lastSeenUpdatedAt: draftSubmission.draftRevision.updatedAt, + } + + const updatedSubmission = await updateDraft(updatedContract) if (updatedSubmission instanceof Error) { setSubmitting(false) console.info( @@ -508,6 +581,8 @@ export const ContractDetails = ({ } } catch (serverError) { setSubmitting(false) + } finally { + setSubmitting(false) } } @@ -517,12 +592,14 @@ export const ContractDetails = ({ <>
@@ -531,10 +608,9 @@ export const ContractDetails = ({ initialValues={contractDetailsInitialValues} onSubmit={(values, { setSubmitting }) => { return handleFormSubmit(values, setSubmitting, { - shouldValidateDocuments: true, redirectPath: - draftSubmission.submissionType === - 'CONTRACT_ONLY' + draftSubmission.draftRevision.formData + .submissionType === 'CONTRACT_ONLY' ? `../contacts` : `../rate-details`, }) @@ -573,21 +649,21 @@ export const ContractDetails = ({ {shouldValidate && ( )} + handleUploadFile( + file, + 'HEALTH_PLAN_DOCS' + ) + } + scanFile={(key) => + handleScanFile( + key, + 'HEALTH_PLAN_DOCS' + ) + } + deleteFile={(key) => + handleDeleteFile( + key, + 'HEALTH_PLAN_DOCS', + previousDocuments + ) + } + onFileItemsUpdate={({ + fileItems, + }) => + setFieldValue( + `contractDocuments`, + fileItems + ) } /> {contract438Attestation && (
- {showFieldErrors( - errors.statutoryRegulatoryAttestation + {Boolean( + showFieldErrors( + 'statutoryRegulatoryAttestation', + errors + ) ) && ( )}
Required - {showFieldErrors( - errors.contractExecutionStatus + {Boolean( + showFieldErrors( + 'contractExecutionStatus', + errors + ) ) && ( { @@ -837,11 +952,17 @@ export const ContractDetails = ({ <> @@ -862,9 +983,17 @@ export const ContractDetails = ({ > Required - {showFieldErrors( - errors.contractDateStart || - errors.contractDateEnd + {Boolean( + showFieldErrors( + 'contractDateStart', + errors + ) || + Boolean( + showFieldErrors( + 'contractDateEnd', + errors + ) + ) ) && (
- {showFieldErrors( - errors.managedCareEntities + {Boolean( + showFieldErrors( + 'managedCareEntities', + errors + ) ) && ( { @@ -1035,8 +1170,11 @@ export const ContractDetails = ({
- {showFieldErrors( - errors.federalAuthorities + {Boolean( + showFieldErrors( + 'federalAuthorities', + errors + ) ) && ( { @@ -1136,10 +1277,11 @@ export const ContractDetails = ({ draftSubmission, modifiedProvisionName )} - showError={showFieldErrors( - errors[ - modifiedProvisionName - ] + showError={Boolean( + showFieldErrors( + modifiedProvisionName, + errors + ) )} variant="SUBHEAD" /> @@ -1154,48 +1296,36 @@ export const ContractDetails = ({ { - // do not need to trigger validations if file list is empty - if (fileItems.length === 0) { - await handleFormSubmit( - values, - setSubmitting, - { - shouldValidateDocuments: - false, - redirectPath: - RoutesRecord.DASHBOARD_SUBMISSIONS, - } - ) - } else { - await handleFormSubmit( - values, - setSubmitting, - { - shouldValidateDocuments: - true, - redirectPath: - RoutesRecord.DASHBOARD_SUBMISSIONS, - } - ) - } + await handleFormSubmit( + values, + setSubmitting, + { + redirectPath: + RoutesRecord.DASHBOARD_SUBMISSIONS, + } + ) }} backOnClick={async () => { // do not need to validate or resubmit if no documents are uploaded - if (fileItems.length === 0) { + if ( + values.contractDocuments.length === + 0 + ) { navigate('../type') } else { await handleFormSubmit( values, setSubmitting, { - shouldValidateDocuments: - false, redirectPath: '../type', } ) } }} - disableContinue={showFileUploadError} + disableContinue={ + shouldValidate && + !!Object.keys(errors).length + } actionInProgress={isSubmitting} backOnClickUrl={generatePath( RoutesRecord.SUBMISSIONS_TYPE, @@ -1205,8 +1335,8 @@ export const ContractDetails = ({ RoutesRecord.DASHBOARD_SUBMISSIONS } continueOnClickUrl={ - draftSubmission.submissionType === - 'CONTRACT_ONLY' + draftSubmission.draftRevision.formData + .submissionType === 'CONTRACT_ONLY' ? '/edit/contacts' : '/edit/rate-details' } diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts index 56d24bab5e..cfd9d3f9f9 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetailsSchema.ts @@ -12,17 +12,21 @@ import { isCHIPOnly, isContractAmendment, isContractWithProvisions, -} from '../../../common-code/healthPlanFormDataType/healthPlanFormData' +} from '../../../common-code/ContractType' import { isMedicaidAmendmentProvision, isMedicaidBaseProvision, } from '../../../common-code/healthPlanFormDataType/ModifiedProvisions' import { FeatureFlagSettings } from '../../../common-code/featureFlags' +import { Contract, UnlockedContract } from '../../../gen/gqlClient' +import { + validateFileItemsList, +} from '../../../formHelpers/validators' Yup.addMethod(Yup.date, 'validateDateFormat', validateDateFormat) export const ContractDetailsFormSchema = ( - draftSubmission: UnlockedHealthPlanFormDataType, + draftSubmission: UnlockedContract, activeFeatureFlags: FeatureFlagSettings = {} ) => { const yesNoError = (provision: GeneralizedProvisionType) => { @@ -79,7 +83,8 @@ export const ContractDetailsFormSchema = ( .validateDateFormat('YYYY-MM-DD', true) .typeError('The start date must be in MM/DD/YYYY format') .defined('You must enter a start date'), - + contractDocuments: validateFileItemsList({ required: true }), + supportingDocuments: validateFileItemsList({ required: false }), contractDateEnd: Yup.date() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-next-line diff --git a/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.test.tsx b/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.test.tsx index 861513ce67..e7d02551c8 100644 --- a/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.test.tsx +++ b/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.test.tsx @@ -2,7 +2,7 @@ import { screen, waitFor, within } from '@testing-library/react' import { fetchCurrentUserMock, - createHealthPlanPackageMockAuthFailure, + createContractMockFail, } from '../../../testHelpers/apolloMocks' import { renderWithProviders } from '../../../testHelpers/jestHelpers' import { NewStateSubmissionForm } from './NewStateSubmissionForm' @@ -30,7 +30,7 @@ describe('NewStateSubmissionForm', () => { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), fetchCurrentUserMock({ statusCode: 200 }), - createHealthPlanPackageMockAuthFailure(), + createContractMockFail({}), ], }, routerProvider: { route: '/submissions/new' }, diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx index 800f64a07c..23ee46dbe4 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx @@ -184,7 +184,7 @@ export const ContractDetailsSummarySection = ({ const lastSubmittedDate = isPreviousSubmission ? getPackageSubmissionAtIndex(contract, lastSubmittedIndex)?.submitInfo .updatedAt - : getLastContractSubmission(contract)?.submitInfo.updatedAt ?? null + : (getLastContractSubmission(contract)?.submitInfo.updatedAt ?? null) return ( { it('renders without errors', async () => { @@ -135,7 +138,10 @@ describe('ReviewSubmit', () => { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), fetchContractMockSuccess({ - contract: { ...mockContractPackageDraft(),id: 'test-abc-123' }, + contract: { + ...mockContractPackageDraft(), + id: 'test-abc-123', + }, }), ], }, @@ -219,7 +225,10 @@ describe('ReviewSubmit', () => { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), fetchContractMockSuccess({ - contract: { ...mockContractPackageDraft(), id: 'test-abc-123' }, + contract: { + ...mockContractPackageDraft(), + id: 'test-abc-123', + }, }), ], }, diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx index 0c5c39d50b..8915cfcd00 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx @@ -10,16 +10,22 @@ import { mockUnlockedHealthPlanPackage, mockUnlockedHealthPlanPackageWithDocuments, } from '../../testHelpers/apolloMocks/healthPlanFormDataMock' +import { + fetchContractMockSuccess, + fetchContractMockFail, + mockContractPackageDraft, + updateContractDraftRevisionMockFail, + mockContractPackageUnlockedWithUnlockedType, +} from '../../testHelpers/apolloMocks' import { fetchHealthPlanPackageMockSuccess, - fetchHealthPlanPackageMockNotFound, fetchHealthPlanPackageMockNetworkFailure, fetchHealthPlanPackageMockAuthFailure, updateHealthPlanFormDataMockSuccess, - updateHealthPlanFormDataMockAuthFailure, } from '../../testHelpers/apolloMocks/healthPlanPackageGQLMock' // some spies will not work with indexed exports, so I refactored to import them directly from their files import { renderWithProviders } from '../../testHelpers/jestHelpers' +import * as useContractForm from '../../hooks/useContractForm' import { StateSubmissionForm } from './StateSubmissionForm' import { @@ -30,10 +36,20 @@ import { testS3Client } from '../../testHelpers/s3Helpers' import { getYesNoFieldValue } from '../../testHelpers/fieldHelpers' import { SubmissionSideNav } from '../SubmissionSideNav' import { fetchStateHealthPlanPackageWithQuestionsMockSuccess } from '../../testHelpers/apolloMocks' +const mockUpdateDraftFn = vi.fn() describe('StateSubmissionForm', () => { + beforeEach(() => { + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: mockContractPackageUnlockedWithUnlockedType(), + }) + }) afterEach(() => { vi.clearAllMocks() + vi.spyOn(useContractForm, 'useContractForm').mockRestore() }) describe('loads draft submission', () => { it('redirects user to submission summary page when status is submitted', async () => { @@ -73,11 +89,27 @@ describe('StateSubmissionForm', () => { }) it('loads submission type fields for /submissions/edit/type', async () => { + const mockDraft = mockContractPackageUnlockedWithUnlockedType() + mockDraft.draftRevision.formData.submissionDescription = + 'A real submission' + mockDraft.draftRevision.formData.submissionType = 'CONTRACT_ONLY' + mockDraft.draftRevision.formData.submissionDescription = + 'A real submission' + mockDraft.draftRevision.formData.programIDs = [ + 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', + ] + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: false, + draftSubmission: mockDraft, + }) const mockSubmission = mockDraftHealthPlanPackage({ submissionDescription: 'A real submission', submissionType: 'CONTRACT_ONLY', programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], }) + renderWithProviders( }> @@ -123,77 +155,6 @@ describe('StateSubmissionForm', () => { }) }) - it('loads contract details fields for /submissions/:id/edit/contract-details with amendments', async () => { - const mockAmendment = mockDraftHealthPlanPackage({ - contractType: 'AMENDMENT', - contractAmendmentInfo: { - modifiedProvisions: { - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: false, - modifiedRiskSharingStrategy: false, - modifiedIncentiveArrangements: false, - modifiedWitholdAgreements: false, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: false, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: false, - modifiedLengthOfContract: false, - modifiedNonRiskPaymentArrangements: false, - inLieuServicesAndSettings: false, - }, - }, - }) - - renderWithProviders( - - }> - } - /> - - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ statusCode: 200 }), - fetchHealthPlanPackageMockSuccess({ - id: '12', - submission: mockAmendment, - }), - fetchStateHealthPlanPackageWithQuestionsMockSuccess( - { - stateSubmission: mockAmendment, - id: '12', - } - ), - ], - }, - routerProvider: { - route: '/submissions/12/edit/contract-details', - }, - } - ) - - await waitFor(() => { - expect( - getYesNoFieldValue( - 'Benefits provided by the managed care plans' - ) - ).toBe(true) - expect( - getYesNoFieldValue( - 'Geographic areas served by the managed care plans' - ) - ).toBe(false) - }) - }) - it('loads documents fields for /submissions/:id/edit/documents', async () => { const mockSubmission = mockDraftHealthPlanPackage() renderWithProviders( @@ -325,6 +286,12 @@ describe('StateSubmissionForm', () => { fetchHealthPlanPackageMockSuccess({ id: '15', }), + fetchContractMockSuccess({ + contract: { + ...mockContractPackageDraft(), + id: '15', + }, + }), fetchStateHealthPlanPackageWithQuestionsMockSuccess( { id: '15', @@ -398,6 +365,12 @@ describe('StateSubmissionForm', () => { pkg: mockSubmission, updatedFormData, }), + fetchContractMockSuccess({ + contract: { + ...mockContractPackageDraft(), + id: '15', + }, + }), fetchHealthPlanPackageMockSuccess({ id: '15', }), @@ -432,6 +405,13 @@ describe('StateSubmissionForm', () => { describe('errors', () => { it('shows a generic error fetching submission fails at submission type', async () => { + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: true, + draftSubmission: undefined, + }) + const mockSubmission = mockDraftHealthPlanPackage() renderWithProviders( @@ -446,13 +426,16 @@ describe('StateSubmissionForm', () => { apolloProvider: { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), - fetchHealthPlanPackageMockAuthFailure(), + // fetchHealthPlanPackageMockAuthFailure(), fetchStateHealthPlanPackageWithQuestionsMockSuccess( { id: '15', stateSubmission: mockSubmission, } ), + fetchContractMockFail({ + id: '15', + }), ], }, routerProvider: { route: '/submissions/15/edit/type' }, @@ -464,6 +447,13 @@ describe('StateSubmissionForm', () => { }) it('shows a generic error fetching submission fails at contract details', async () => { + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: true, + draftSubmission: undefined, + }) + const mockSubmission = mockDraftHealthPlanPackage() renderWithProviders( @@ -535,6 +525,13 @@ describe('StateSubmissionForm', () => { 'A real submission but updated something', }) + vi.spyOn(useContractForm, 'useContractForm').mockReturnValue({ + updateDraft: mockUpdateDraftFn, + createDraft: vi.fn(), + showPageErrorMessage: true, + draftSubmission: undefined, + }) + renderWithProviders( }> @@ -552,13 +549,15 @@ describe('StateSubmissionForm', () => { submission: mockSubmission, id: '15', }), - updateHealthPlanFormDataMockAuthFailure(), fetchStateHealthPlanPackageWithQuestionsMockSuccess( { id: '15', stateSubmission: mockSubmission, } ), + updateContractDraftRevisionMockFail({ + contract: { id: '15' }, + }), ], }, routerProvider: { route: '/submissions/15/edit/type' }, @@ -579,7 +578,7 @@ describe('StateSubmissionForm', () => { name: 'Continue', }) expect(continueButton).toBeInTheDocument() - continueButton.click() + await continueButton.click() await waitFor(() => { expect(screen.getByText('System error')).toBeInTheDocument() @@ -587,7 +586,6 @@ describe('StateSubmissionForm', () => { }) it('shows a generic 404 page when package is not found', async () => { - const mockSubmission = mockDraftHealthPlanPackage() renderWithProviders( }> @@ -601,22 +599,17 @@ describe('StateSubmissionForm', () => { apolloProvider: { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), - fetchHealthPlanPackageMockNotFound({ + fetchContractMockSuccess({}), + fetchContractMockFail({ id: '404', }), - fetchStateHealthPlanPackageWithQuestionsMockSuccess( - { - id: '404', - stateSubmission: mockSubmission, - } - ), ], }, routerProvider: { route: '/submissions/404/edit/type' }, } ) - const notFound = await screen.findByText('404 / Page not found') + const notFound = await screen.findByText('System error') expect(notFound).toBeInTheDocument() }) }) @@ -683,5 +676,59 @@ describe('StateSubmissionForm', () => { // in the deleteCallKeys array. expect(deleteCallKeys).toEqual(['three-one']) }) + + it('loads contract details fields for /submissions/:id/edit/contract-details with amendments', async () => { + const mockSubmission = mockDraftHealthPlanPackage() + + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ statusCode: 200 }), + fetchHealthPlanPackageMockSuccess({ + id: '12', + submission: mockSubmission, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess( + { + stateSubmission: mockSubmission, + id: '12', + } + ), + fetchContractMockSuccess({ + contract: { + ...mockContractPackageDraft(), + id: '12', + }, + }), + ], + }, + routerProvider: { + route: '/submissions/12/edit/contract-details', + }, + } + ) + + await waitFor(() => { + expect( + getYesNoFieldValue( + 'Benefits provided by the managed care plans' + ) + ).toBe(true) + expect( + getYesNoFieldValue( + 'Geographic areas served by the managed care plans' + ) + ).toBe(false) + }) + }) }) }) diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx index 36077bb0ba..912d2c1bac 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx @@ -112,3 +112,7 @@ const getRelativePathFromNestedRoute = (formRouteType: RouteT): string => export type HealthPlanFormPageProps = { showValidations?: boolean } + +export type ContractFormPageProps = { + showValidations?: boolean +} diff --git a/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.test.tsx b/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.test.tsx index 4fec0e5e9d..e28adec941 100644 --- a/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.test.tsx +++ b/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.test.tsx @@ -2,39 +2,15 @@ import userEvent from '@testing-library/user-event' import { screen, waitFor, within } from '@testing-library/react' import selectEvent from 'react-select-event' -import { fetchCurrentUserMock } from '../../../testHelpers/apolloMocks' +import { + fetchCurrentUserMock, + fetchContractMockSuccess, + mockContractPackageDraft, +} from '../../../testHelpers/apolloMocks' import { renderWithProviders } from '../../../testHelpers/jestHelpers' import { SubmissionType } from './' -import { contractOnly } from '../../../common-code/healthPlanFormDataMocks' -import * as useRouteParams from '../../../hooks/useRouteParams' -import * as useHealthPlanPackageForm from '../../../hooks/useHealthPlanPackageForm' -// set up mocks for React Hooks in use -const mockUpdateDraftFn = vi.fn() -const mockCreateDraftFn = vi.fn() describe('SubmissionType', () => { - beforeEach(() => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: contractOnly(), - }) - vi.spyOn(useRouteParams, 'useRouteParams').mockReturnValue({ - id: '123-abc', - }) - }) - afterEach(() => { - vi.clearAllMocks() - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockRestore() - vi.spyOn(useRouteParams, 'useRouteParams').mockRestore() - }) it('displays correct form guidance', async () => { renderWithProviders(, { apolloProvider: { @@ -51,7 +27,15 @@ describe('SubmissionType', () => { it('displays submission type form when expected', async () => { renderWithProviders(, { apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], + mocks: [ + fetchCurrentUserMock({ statusCode: 200 }), + fetchContractMockSuccess({ + contract: { + ...mockContractPackageDraft(), + id: '15', + }, + }), + ], }, }) @@ -80,9 +64,7 @@ describe('SubmissionType', () => { apolloProvider: { mocks: [fetchCurrentUserMock({ statusCode: 200 })], }, - routerProvider: { - route: '/submissions/new', - }, + routerProvider: { route: '/submissions/new' }, }) expect( @@ -214,16 +196,6 @@ describe('SubmissionType', () => { ).toBeInTheDocument() }) it('new submissions does not automatically select contract only submission type when selecting CHIP-only coverage', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) - renderWithProviders(, { apolloProvider: { mocks: [fetchCurrentUserMock({ statusCode: 200 })], @@ -317,15 +289,6 @@ describe('SubmissionType', () => { expect(contractOnlyRadio).toBeChecked() }) it('shows validation message when population coverage is not selected', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) renderWithProviders(, { apolloProvider: { mocks: [fetchCurrentUserMock({ statusCode: 200 })], @@ -463,15 +426,6 @@ describe('SubmissionType', () => { }) it('displays risk-based contract radio buttons and validation message', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) renderWithProviders(, { apolloProvider: { mocks: [fetchCurrentUserMock({ statusCode: 200 })], @@ -539,15 +493,6 @@ describe('SubmissionType', () => { }) it('shows error messages when there are validation errors and showValidations is true', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) renderWithProviders( , @@ -576,15 +521,6 @@ describe('SubmissionType', () => { }) it('shows error messages when contract type is not selected', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) renderWithProviders( , @@ -641,15 +577,6 @@ describe('SubmissionType', () => { }) it('if form fields are invalid, shows validation error messages when continue button is clicked', async () => { - vi.spyOn( - useHealthPlanPackageForm, - 'useHealthPlanPackageForm' - ).mockReturnValue({ - updateDraft: mockUpdateDraftFn, - createDraft: mockCreateDraftFn, - showPageErrorMessage: false, - draftSubmission: undefined, - }) renderWithProviders(, { apolloProvider: { mocks: [fetchCurrentUserMock({ statusCode: 200 })], diff --git a/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.tsx b/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.tsx index ad60ab8744..7a0dd24b1e 100644 --- a/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.tsx +++ b/services/app-web/src/pages/StateSubmission/SubmissionType/SubmissionType.tsx @@ -5,7 +5,7 @@ import { Label, } from '@trussworks/react-uswds' import { Formik, FormikErrors, FormikHelpers } from 'formik' -import React, { useEffect } from 'react' +import React, { useState } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { DynamicStepIndicator, @@ -16,6 +16,7 @@ import { PoliteErrorMessage, ReactRouterLinkWithLogging, } from '../../../components' +import { isContractWithProvisions } from '../../../common-code/ContractType' import { PopulationCoveredRecord, SubmissionTypeRecord, @@ -26,14 +27,16 @@ import { } from '../../../common-code/healthPlanFormDataType' import { SubmissionType as SubmissionTypeT, - CreateHealthPlanPackageInput, + CreateContractInput, + ContractDraftRevisionFormDataInput, + UpdateContractDraftRevisionInput, } from '../../../gen/gqlClient' import { PageActions } from '../PageActions' import styles from '../StateSubmissionForm.module.scss' import { GenericApiErrorBanner, ProgramSelect } from '../../../components' import { - type HealthPlanFormPageProps, activeFormPages, + type ContractFormPageProps, } from '../StateSubmissionForm' import { booleanAsYesNoFormValue, @@ -42,12 +45,13 @@ import { import { SubmissionTypeFormSchema } from './SubmissionTypeSchema' import { RoutesRecord, STATE_SUBMISSION_FORM_ROUTES } from '../../../constants' import { FormContainer } from '../FormContainer' -import { useHealthPlanPackageForm } from '../../../hooks/useHealthPlanPackageForm' import { useCurrentRoute } from '../../../hooks' import { ErrorOrLoadingPage } from '../ErrorOrLoadingPage' import { useAuth } from '../../../contexts/AuthContext' import { useRouteParams } from '../../../hooks/useRouteParams' import { PageBannerAlerts } from '../PageBannerAlerts' +import { useErrorSummary } from '../../../hooks/useErrorSummary' +import { useContractForm } from '../../../hooks/useContractForm' export interface SubmissionTypeFormValues { populationCovered?: PopulationCoveredType @@ -60,20 +64,22 @@ export interface SubmissionTypeFormValues { type FormError = FormikErrors[keyof FormikErrors] + export const SubmissionType = ({ showValidations = false, -}: HealthPlanFormPageProps): React.ReactElement => { +}: ContractFormPageProps): React.ReactElement => { const { loggedInUser } = useAuth() const { currentRoute } = useCurrentRoute() - const [showFormAlert, setShowFormAlert] = React.useState(false) - const [shouldValidate, setShouldValidate] = React.useState(showValidations) - const errorSummaryHeadingRef = React.useRef(null) - const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = - React.useState(false) + const [shouldValidate, setShouldValidate] = useState(showValidations) + const [showAPIErrorBanner, setShowAPIErrorBanner] = useState< + boolean | string + >(false) // string is a custom error message, defaults to generic message when true + + const { setFocusErrorSummaryHeading, errorSummaryHeadingRef } = + useErrorSummary() const navigate = useNavigate() const location = useLocation() const isNewSubmission = location.pathname === '/submissions/new' - const { id } = useRouteParams() const { @@ -82,39 +88,40 @@ export const SubmissionType = ({ createDraft, interimState, showPageErrorMessage, - unlockInfo, - } = useHealthPlanPackageForm(id) - - useEffect(() => { - // Focus the error summary heading only if we are displaying - // validation errors and the heading element exists - if (focusErrorSummaryHeading && errorSummaryHeadingRef.current) { - errorSummaryHeadingRef.current.focus() - } - setFocusErrorSummaryHeading(false) - }, [focusErrorSummaryHeading]) + } = useContractForm(id) const showFieldErrors = (error?: FormError) => shouldValidate && Boolean(error) const submissionTypeInitialValues: SubmissionTypeFormValues = { - populationCovered: draftSubmission?.populationCovered, - programIDs: draftSubmission?.programIDs ?? [], + populationCovered: + draftSubmission?.draftRevision?.formData.populationCovered === null + ? undefined + : draftSubmission?.draftRevision?.formData.populationCovered, + programIDs: draftSubmission?.draftRevision?.formData.programIDs ?? [], riskBasedContract: - booleanAsYesNoFormValue(draftSubmission?.riskBasedContract) ?? '', - submissionDescription: draftSubmission?.submissionDescription ?? '', - submissionType: draftSubmission?.submissionType ?? '', - contractType: draftSubmission?.contractType ?? '', + booleanAsYesNoFormValue( + draftSubmission?.draftRevision?.formData.riskBasedContract === + null + ? undefined + : draftSubmission?.draftRevision?.formData.riskBasedContract + ) ?? '', + submissionDescription: + draftSubmission?.draftRevision?.formData.submissionDescription ?? + '', + submissionType: + draftSubmission?.draftRevision?.formData.submissionType ?? '', + contractType: + draftSubmission?.draftRevision?.formData.contractType ?? '', } - if (interimState) + if (interimState) { return + } + const handleFormSubmit = async ( values: SubmissionTypeFormValues, - formikHelpers: Pick< - FormikHelpers, - 'setSubmitting' - >, + setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting redirectPath?: string ) => { if (isNewSubmission) { @@ -151,16 +158,17 @@ export const SubmissionType = ({ return } - const input: CreateHealthPlanPackageInput = { - populationCovered: values.populationCovered, + const input: CreateContractInput = { + populationCovered: values.populationCovered!, programIDs: values.programIDs, - submissionType: values.submissionType, + submissionType: values.submissionType as SubmissionTypeT, riskBasedContract: yesNoFormValueAsBoolean( values.riskBasedContract ), submissionDescription: values.submissionDescription, - contractType: values.contractType, + contractType: values.contractType as ContractType, } + if (!createDraft) { console.info( 'PROGRAMMING ERROR, SubmissionType for does have props needed to update a draft.' @@ -178,12 +186,14 @@ export const SubmissionType = ({ `/submissions/${draftSubmission.id}/edit/contract-details` ) } catch (serverError) { - setShowFormAlert(true) - formikHelpers.setSubmitting(false) // unblock submit button to allow resubmit + setShowAPIErrorBanner(true) + setSubmitting(false) // unblock submit button to allow resubmit console.info( 'Log: creating new submission failed with server error', serverError ) + } finally { + setSubmitting(false) } } else { if (draftSubmission === undefined || !updateDraft) { @@ -193,27 +203,137 @@ export const SubmissionType = ({ ) return } - // set new values - draftSubmission.populationCovered = values.populationCovered - draftSubmission.programIDs = values.programIDs - draftSubmission.submissionType = - values.submissionType as SubmissionTypeT - draftSubmission.riskBasedContract = yesNoFormValueAsBoolean( - values.riskBasedContract - ) - draftSubmission.submissionDescription = values.submissionDescription - draftSubmission.contractType = values.contractType as ContractType + const updatedDraftSubmissionFormData: ContractDraftRevisionFormDataInput = + { + contractExecutionStatus: + draftSubmission.draftRevision.formData + .contractExecutionStatus, + contractDateStart: + draftSubmission.draftRevision.formData + .contractDateStart, + contractDateEnd: + draftSubmission.draftRevision.formData.contractDateEnd, + contractType: values.contractType as ContractType, + submissionDescription: values.submissionDescription, + riskBasedContract: yesNoFormValueAsBoolean( + values.riskBasedContract + ), + populationCovered: values.populationCovered, + submissionType: values.submissionType as SubmissionTypeT, + programIDs: values.programIDs, + stateContacts: + draftSubmission.draftRevision.formData.stateContacts || + [], + supportingDocuments: + draftSubmission.draftRevision.formData + .supportingDocuments || [], + managedCareEntities: + draftSubmission.draftRevision.formData + .managedCareEntities, + federalAuthorities: + draftSubmission.draftRevision.formData + .federalAuthorities, + contractDocuments: + draftSubmission.draftRevision.formData + .contractDocuments, + statutoryRegulatoryAttestation: + draftSubmission.draftRevision.formData + .statutoryRegulatoryAttestation, + // If contract is in compliance, we set the description to undefined. This clears out previous non-compliance description + statutoryRegulatoryAttestationDescription: + draftSubmission.draftRevision.formData + .statutoryRegulatoryAttestationDescription, + } + + if (isContractWithProvisions(draftSubmission)) { + updatedDraftSubmissionFormData.inLieuServicesAndSettings = + draftSubmission.draftRevision.formData.inLieuServicesAndSettings + updatedDraftSubmissionFormData.modifiedBenefitsProvided = + draftSubmission.draftRevision.formData.modifiedBenefitsProvided + updatedDraftSubmissionFormData.modifiedGeoAreaServed = + draftSubmission.draftRevision.formData.modifiedGeoAreaServed + updatedDraftSubmissionFormData.modifiedMedicaidBeneficiaries = + draftSubmission.draftRevision.formData.modifiedMedicaidBeneficiaries + updatedDraftSubmissionFormData.modifiedRiskSharingStrategy = + draftSubmission.draftRevision.formData.modifiedRiskSharingStrategy + updatedDraftSubmissionFormData.modifiedIncentiveArrangements = + draftSubmission.draftRevision.formData.modifiedIncentiveArrangements + updatedDraftSubmissionFormData.modifiedWitholdAgreements = + draftSubmission.draftRevision.formData.modifiedWitholdAgreements + updatedDraftSubmissionFormData.modifiedStateDirectedPayments = + draftSubmission.draftRevision.formData.modifiedStateDirectedPayments + updatedDraftSubmissionFormData.modifiedPassThroughPayments = + draftSubmission.draftRevision.formData.modifiedPassThroughPayments + updatedDraftSubmissionFormData.modifiedPaymentsForMentalDiseaseInstitutions = + draftSubmission.draftRevision.formData.modifiedPaymentsForMentalDiseaseInstitutions + updatedDraftSubmissionFormData.modifiedMedicalLossRatioStandards = + draftSubmission.draftRevision.formData.modifiedMedicalLossRatioStandards + updatedDraftSubmissionFormData.modifiedOtherFinancialPaymentIncentive = + draftSubmission.draftRevision.formData.modifiedOtherFinancialPaymentIncentive + updatedDraftSubmissionFormData.modifiedEnrollmentProcess = + draftSubmission.draftRevision.formData.modifiedEnrollmentProcess + updatedDraftSubmissionFormData.modifiedGrevienceAndAppeal = + draftSubmission.draftRevision.formData.modifiedGrevienceAndAppeal + updatedDraftSubmissionFormData.modifiedNetworkAdequacyStandards = + draftSubmission.draftRevision.formData.modifiedNetworkAdequacyStandards + updatedDraftSubmissionFormData.modifiedLengthOfContract = + draftSubmission.draftRevision.formData.modifiedLengthOfContract + updatedDraftSubmissionFormData.modifiedNonRiskPaymentArrangements = + draftSubmission.draftRevision.formData.modifiedNonRiskPaymentArrangements + } else { + updatedDraftSubmissionFormData.inLieuServicesAndSettings = + undefined + updatedDraftSubmissionFormData.modifiedBenefitsProvided = + undefined + updatedDraftSubmissionFormData.modifiedGeoAreaServed = undefined + updatedDraftSubmissionFormData.modifiedMedicaidBeneficiaries = + undefined + updatedDraftSubmissionFormData.modifiedRiskSharingStrategy = + undefined + updatedDraftSubmissionFormData.modifiedIncentiveArrangements = + undefined + updatedDraftSubmissionFormData.modifiedWitholdAgreements = + undefined + updatedDraftSubmissionFormData.modifiedStateDirectedPayments = + undefined + updatedDraftSubmissionFormData.modifiedPassThroughPayments = + undefined + updatedDraftSubmissionFormData.modifiedPaymentsForMentalDiseaseInstitutions = + undefined + updatedDraftSubmissionFormData.modifiedMedicalLossRatioStandards = + undefined + updatedDraftSubmissionFormData.modifiedOtherFinancialPaymentIncentive = + undefined + updatedDraftSubmissionFormData.modifiedEnrollmentProcess = + undefined + updatedDraftSubmissionFormData.modifiedGrevienceAndAppeal = + undefined + updatedDraftSubmissionFormData.modifiedNetworkAdequacyStandards = + undefined + updatedDraftSubmissionFormData.modifiedLengthOfContract = + undefined + updatedDraftSubmissionFormData.modifiedNonRiskPaymentArrangements = + undefined + } try { - const updatedDraft = await updateDraft(draftSubmission) + const updatedContractInput: UpdateContractDraftRevisionInput = { + formData: updatedDraftSubmissionFormData, + contractID: draftSubmission.id, + lastSeenUpdatedAt: draftSubmission.draftRevision.updatedAt, + } + const updatedDraft = await updateDraft(updatedContractInput) if (updatedDraft instanceof Error) { - formikHelpers.setSubmitting(false) + setSubmitting(false) } else { navigate(redirectPath || `../contract-details`) } } catch (serverError) { - formikHelpers.setSubmitting(false) // unblock submit button to allow resubmit + setShowAPIErrorBanner(true) + setSubmitting(false) // unblock submit button to allow resubmit + } finally { + setSubmitting(false) } } } @@ -258,21 +378,25 @@ export const SubmissionType = ({ { + return handleFormSubmit(values, setSubmitting) + }} validationSchema={SubmissionTypeFormSchema()} > {({ @@ -282,179 +406,154 @@ export const SubmissionType = ({ isSubmitting, setSubmitting, setFieldValue, - }) => ( - <> - -
- - Submission type - - {showFormAlert && } - - {shouldValidate && ( - - )} - { + return ( + <> + +
+ + Submission type + + {showAPIErrorBanner && ( + )} - className="margin-top-0" - > -
- - Required - - {showFieldErrors( + /> + )} + - {errors.populationCovered} - )} - - handlePopulationCoveredClick( - 'MEDICAID', - values, - setFieldValue - ) - } + className="margin-top-0" + > +
- - handlePopulationCoveredClick( - 'MEDICAID_AND_CHIP', - values, - setFieldValue - ) - } - list_position={2} - list_options={3} - parent_component_heading="Which populations does this contract action cover?" - radio_button_title={ - PopulationCoveredRecord[ - 'MEDICAID_AND_CHIP' - ] - } - /> - - handlePopulationCoveredClick( - 'CHIP', - values, - setFieldValue - ) - } - list_position={3} - list_options={3} - parent_component_heading="Which populations does this contract action cover?" - radio_button_title={ - PopulationCoveredRecord[ - 'CHIP' - ] - } - /> -
-
+ legend="Which populations does this contract action cover?" + > + + Required + + {showFieldErrors( + errors.populationCovered + ) && ( + + { + errors.populationCovered + } + + )} + + handlePopulationCoveredClick( + 'MEDICAID', + values, + setFieldValue + ) + } + aria-required + list_position={1} + list_options={3} + parent_component_heading="Which populations does this contract action cover?" + radio_button_title={ + PopulationCoveredRecord[ + 'MEDICAID' + ] + } + /> + + handlePopulationCoveredClick( + 'MEDICAID_AND_CHIP', + values, + setFieldValue + ) + } + list_position={2} + list_options={3} + parent_component_heading="Which populations does this contract action cover?" + radio_button_title={ + PopulationCoveredRecord[ + 'MEDICAID_AND_CHIP' + ] + } + /> + + handlePopulationCoveredClick( + 'CHIP', + values, + setFieldValue + ) + } + list_position={3} + list_options={3} + parent_component_heading="Which populations does this contract action cover?" + radio_button_title={ + PopulationCoveredRecord[ + 'CHIP' + ] + } + /> +
+ - - - - Required - - {showFieldErrors(errors.programIDs) && ( - - {errors.programIDs} - - )} - - - -
+ {showFieldErrors( - errors.submissionType + errors.programIDs ) && ( - - {errors.submissionType} + + {errors.programIDs} )} - - - {values.populationCovered === - 'CHIP' && ( -
- States are not required to - submit rates with CHIP-only - contracts. -
+ + - - -
- - Required - - {showFieldErrors( + + Required + + {showFieldErrors( + errors.submissionType + ) && ( + + {errors.submissionType} + + )} + + + {values.populationCovered === + 'CHIP' && ( +
+ States are not required + to submit rates with + CHIP-only contracts. +
+ )} +
+
+ - {errors.contractType} -
)} - +
- + + Required + + {showFieldErrors( + errors.contractType + ) && ( + + {errors.contractType} + + )} + + +
+ + + -
-
- - + +

+ Provide a 1-2 paragraph + summary of your + submission that + highlights any important + changes CMS reviewers + will need to be aware of +

+ + View description + examples + + + } /> -
- -

- Provide a 1-2 paragraph - summary of your submission - that highlights any - important changes CMS - reviewers will need to be - aware of -

- - View description examples - - +
+ -
- - navigate( + backOnClick={() => + navigate( + RoutesRecord.DASHBOARD_SUBMISSIONS + ) + } + continueOnClick={() => { + setShouldValidate(true) + setFocusErrorSummaryHeading(true) + }} + saveAsDraftOnClick={async () => { + await handleFormSubmit( + values, + setSubmitting, + RoutesRecord.DASHBOARD_SUBMISSIONS + ) + }} + actionInProgress={isSubmitting} + backOnClickUrl={ RoutesRecord.DASHBOARD_SUBMISSIONS - ) - } - continueOnClick={() => { - setShouldValidate(true) - setFocusErrorSummaryHeading(true) - }} - saveAsDraftOnClick={async () => { - await handleFormSubmit( - values, - { setSubmitting }, + } + saveAsDraftOnClickUrl={ RoutesRecord.DASHBOARD_SUBMISSIONS - ) - }} - actionInProgress={isSubmitting} - backOnClickUrl={ - RoutesRecord.DASHBOARD_SUBMISSIONS - } - saveAsDraftOnClickUrl={ - RoutesRecord.DASHBOARD_SUBMISSIONS - } - continueOnClickUrl="/edit/contract-details" - /> -
- - )} + } + continueOnClickUrl="/edit/contract-details" + /> + + + ) + }}
diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx index 5813143c05..02f9d0ec9f 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx @@ -310,7 +310,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -353,7 +352,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -396,7 +394,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -431,7 +428,6 @@ describe('SubmissionSideNav', () => { ], }, routerProvider: { route: '/submissions/404' }, - } ) @@ -470,7 +466,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15/question-and-answers', }, - } ) diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx index 59bee1725a..45ae4a77ad 100644 --- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx +++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx @@ -1,6 +1,5 @@ import { GridContainer, - Icon, Link, ModalRef, ModalToggleButton, @@ -15,7 +14,6 @@ import { SubmissionUnlockedBanner, SubmissionUpdatedBanner, DocumentWarningBanner, - NavLinkWithLogging, LinkWithLogging, } from '../../components' import { Loading } from '../../components' @@ -202,52 +200,50 @@ export const SubmissionSummary = (): React.ReactElement => { )} - - {contract.mccrsID && ( - - MC-CRS record number: - - {contract.mccrsID} - - - )} - - {editOrAddMCCRSID} - - - ) : undefined - } - contract={contract} - submissionName={name} - headerChildComponent={ - hasCMSPermissions ? ( - - ) : undefined - } - statePrograms={statePrograms} - initiallySubmittedAt={contract.initiallySubmittedAt} - isStateUser={isStateUser} - explainMissingData={explainMissingData} - /> + + {contract.mccrsID && ( + + MC-CRS record number: + + {contract.mccrsID} + + + )} + + {editOrAddMCCRSID} + + + ) : undefined + } + contract={contract} + submissionName={name} + headerChildComponent={ + hasCMSPermissions ? ( + + ) : undefined + } + statePrograms={statePrograms} + initiallySubmittedAt={contract.initiallySubmittedAt} + isStateUser={isStateUser} + explainMissingData={explainMissingData} + /> { => { + const graphQLError = new GraphQLError( + error + ? GRAPHQL_ERROR_CAUSE_MESSAGES[error.cause] + : 'Error attempting to submit.', + { + extensions: { + code: error?.code, + cause: error?.cause, + }, + } + ) + + return { + request: { + query: FetchContractDocument, + variables: { input: { contractID: id } }, + }, + error: new ApolloError({ + graphQLErrors: [graphQLError], + }), + result: { + data: null, + errors: [graphQLError], + }, + } +} + +const createContractMockFail = ({ + error, +}: { + error?: { + code: GraphQLErrorCodeTypes + cause: GraphQLErrorCauseTypes + } +}): MockedResponse => { + const graphQLError = new GraphQLError( + error + ? GRAPHQL_ERROR_CAUSE_MESSAGES[error.cause] + : 'Error attempting to submit.', + { + extensions: { + code: error?.code, + cause: error?.cause, + }, + } + ) + + return { + request: { + query: CreateContractDocument, + variables: { input: { contractID: '123' } }, + }, + error: new ApolloError({ + graphQLErrors: [graphQLError], + }), + result: { + data: null, + errors: [graphQLError], + }, + } +} + +const createContractMockSuccess = ({ + contract, +}: { + contract?: Partial +}): MockedResponse => { + const contractData = mockContractPackageDraft(contract) + + return { + request: { + query: FetchContractDocument, + variables: { input: { contractID: contractData.id } }, + }, + result: { + data: { + createContract: { + contract: { + ...contractData, + }, + }, + }, + }, + } +} + const updateDraftContractRatesMockSuccess = ({ contract, }: { @@ -93,6 +193,77 @@ const updateDraftContractRatesMockSuccess = ({ } } +const updateContractDraftRevisionMockSuccess = ({ + contract, +}: { + contract?: Partial +}): MockedResponse => { + const contractData = mockContractPackageDraft(contract) + const contractInput = { + contractID: contractData.id, + lastSeenUpdatedAt: contractData.draftRevision?.updatedAt, + formData: contractData.draftRevision?.formData + } + return { + request: { + query: UpdateDraftContractRatesDocument, + variables: { input: contractInput }, + }, + result: { + data: { + updateContractDraftRevision: { + contract: { + ...contractData, + }, + }, + }, + }, + } +} +const updateContractDraftRevisionMockFail = ({ + contract, + error +}: { + contract?: Partial + error?: { + code: GraphQLErrorCodeTypes + cause: GraphQLErrorCauseTypes + } +}): MockedResponse => { + const contractData = mockContractPackageDraft(contract) + const contractInput = { + contractID: contractData.id, + lastSeenUpdatedAt: contractData.draftRevision?.updatedAt, + formData: contractData.draftRevision?.formData + } + + const graphQLError = new GraphQLError( + error + ? GRAPHQL_ERROR_CAUSE_MESSAGES[error.cause] + : 'Error attempting to update', + { + extensions: { + code: error?.code, + cause: error?.cause, + }, + } + ) + + return { + request: { + query: UpdateContractDraftRevisionDocument, + variables: { input: contractInput }, + }, + error: new ApolloError({ + graphQLErrors: [graphQLError], + }), + result: { + data: null, + errors: [graphQLError], + }, + } +} + const submitContractMockSuccess = ({ id, submittedReason, @@ -146,4 +317,4 @@ const submitContractMockError = ({ }, } } -export { fetchContractMockSuccess, updateDraftContractRatesMockSuccess, submitContractMockSuccess, submitContractMockError } +export { fetchContractMockSuccess, fetchContractMockFail, updateDraftContractRatesMockSuccess, updateContractDraftRevisionMockFail, updateContractDraftRevisionMockSuccess, submitContractMockSuccess, submitContractMockError, createContractMockFail, createContractMockSuccess } diff --git a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts index 2fce3e4d63..72e112e925 100644 --- a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts @@ -1,5 +1,5 @@ import { mockMNState } from '../../common-code/healthPlanFormDataMocks/healthPlanFormData' -import { Contract, ContractFormData, ContractRevision, RateRevision } from '../../gen/gqlClient' +import { Contract, ContractFormData, ContractRevision, RateRevision, UnlockedContract } from '../../gen/gqlClient' import { s3DlUrl } from './documentDataMock' @@ -1866,6 +1866,370 @@ function mockContractPackageUnlocked( } } +function mockContractPackageUnlockedWithUnlockedType( + partial?: Partial +): UnlockedContract { + return { + status: 'UNLOCKED', + __typename: 'UnlockedContract', + createdAt: '2023-01-01T16:54:39.173Z', + updatedAt: '2024-12-01T16:54:39.173Z', + initiallySubmittedAt:'2023-01-01', + id: 'test-abc-123', + stateCode: 'MN', + state: mockMNState(), + stateNumber: 5, + mccrsID: '1234', + draftRevision: { + __typename: 'ContractRevision', + submitInfo: undefined, + unlockInfo: { + updatedAt: '2023-01-01T16:54:39.173Z', + updatedBy: { + email: 'cms@example.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'unlocked for a test', + }, + id: '123', + createdAt: new Date(), + updatedAt: new Date(), + contractName: 'MCR-MN-0005-SNBC', + formData: { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: true, + submissionDescription: 'An updated submission', + supportingDocuments: [], + stateContacts: [ + { + name: 'State Contact 1', + titleRole: 'Test State Contact 1', + email: 'actuarycontact1@test.com', + }, + ], + contractType: 'AMENDMENT', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [ + { + s3URL: 's3://bucketname/one-two/one-two.png', + sha256: 'fakesha', + name: 'one two', + dateAdded: new Date('02/02/2023') + }, + ], + contractDateStart: new Date('02/02/2023'), + contractDateEnd: new Date('02/02/2024'), + 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" + } + }, + + draftRates: [ + { + id: '123', + createdAt: new Date(), + updatedAt: new Date(), + status: 'SUBMITTED', + stateCode: 'MN', + revisions: [], + state: mockMNState(), + stateNumber: 5, + parentContractID: 'test-abc-123', + draftRevision: { + id: '123', + rateID: '456', + contractRevisions: [], + createdAt: new Date(), + updatedAt: new Date(), + unlockInfo: { + updatedAt: new Date(), + updatedBy: { + email: 'cms@example.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'unlocked for a test', + }, + formData: { + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://bucketname/key/rate', + sha256: 'fakesha', + name: 'rate', + dateAdded: new Date('03/02/2023') + }, + ], + supportingDocuments: [], + rateDateStart: new Date('2020-02-02'), + rateDateEnd: new Date('2021-02-02'), + rateDateCertified: new Date(), + amendmentEffectiveDateStart: new Date(), + amendmentEffectiveDateEnd: new Date(), + rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + deprecatedRateProgramIDs: ['d95394e5-44d1-45df-8151-1cc1ee66f10'], + 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: 'additionalactuarycontact1@test.com', + }, + ], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + packagesWithSharedRateCerts: [], + } + } + + }, + ], + packageSubmissions: [{ + cause: 'CONTRACT_SUBMISSION', + submitInfo: { + updatedAt: '2023-01-01T16:54:39.173Z', + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'initial submission' + }, + submittedRevisions: [ + { + contractName: 'MCR-MN-0005-SNBC', + createdAt: new Date('01/01/2024'), + updatedAt: '2023-01-01T16:54:39.173Z', + submitInfo: { + updatedAt: '2023-01-01T16:54:39.173Z', + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'initial submission' + }, + unlockInfo: { + updatedAt: '2023-01-01T16:54:39.173Z', + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'unlocked for a test' + }, + id: '123', + formData: { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: true, + submissionDescription: 'An initial submission', + supportingDocuments: [], + stateContacts: [], + contractType: 'AMENDMENT', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [ + { + s3URL: 's3://bucketname/key/contract', + sha256: 'fakesha', + name: 'contract', + dateAdded: new Date() + }, + ], + contractDateStart: new Date('01/01/2023'), + contractDateEnd: new Date('01/01/2024'), + 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" + } + } + ], + contractRevision: { + contractName: 'MCR-MN-0005-SNBC', + createdAt: new Date('01/01/2024'), + updatedAt: '2024-01-01T18:54:39.173Z', + submitInfo: { + updatedAt: '2024-01-01T18:54:39.173Z', + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'initial submission' + }, + unlockInfo: { + updatedAt: '2024-02-01T16:54:39.173Z', + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'unlocked' + }, + id: '123', + formData: { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: true, + submissionDescription: 'An initial submission', + supportingDocuments: [], + stateContacts: [], + contractType: 'AMENDMENT', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [ + { + s3URL: 's3://bucketname/key/contract', + sha256: 'fakesha', + name: 'contract', + dateAdded: new Date() + }, + ], + contractDateStart: new Date('01/01/2023'), + contractDateEnd: new Date('01/01/2024'), + 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', + rateID: '456', + createdAt: new Date('01/01/2023'), + updatedAt: new Date('01/01/2023'), + submitInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: { + email: 'example@state.com', + role: 'STATE_USER', + givenName: 'John', + familyName: 'Vila' + }, + updatedReason: 'initial submission' + }, + contractRevisions: [], + formData: { + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://bucketname/key/rate', + sha256: 'fakesha', + name: 'rate', + dateAdded: new Date() + }, + ], + supportingDocuments: [], + rateDateStart: new Date('2020-01-01'), + rateDateEnd: new Date('2021-01-01'), + rateDateCertified: new Date(), + amendmentEffectiveDateStart: new Date(), + amendmentEffectiveDateEnd: new Date(), + rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + deprecatedRateProgramIDs: ['ea16a6c0-5fc6-4df8-adac-c627e76660ab'], + 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: [ { + packageName: 'testABC1', + packageId: 'test-abc-1', + },] + } + }, + ], + }], + ...partial, + } +} function mockContractFormData( partial?: Partial): ContractFormData { return { programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], @@ -2089,6 +2453,7 @@ export { mockContractPackageSubmittedWithRevisions, mockContractPackageWithDifferentProgramsInRevisions, mockEmptyDraftContractAndRate, + mockContractPackageUnlockedWithUnlockedType, mockContractRevision, mockRateRevision } diff --git a/services/app-web/src/testHelpers/apolloMocks/index.ts b/services/app-web/src/testHelpers/apolloMocks/index.ts index 563234b0d9..b9e326d391 100644 --- a/services/app-web/src/testHelpers/apolloMocks/index.ts +++ b/services/app-web/src/testHelpers/apolloMocks/index.ts @@ -75,11 +75,12 @@ export { mockContractPackageUnlocked, mockContractPackageSubmittedWithRevisions, mockEmptyDraftContractAndRate, + mockContractPackageUnlockedWithUnlockedType, mockContractRevision, mockRateRevision } from './contractPackageDataMock' export { rateDataMock } from './rateDataMock' -export { fetchContractMockSuccess, updateDraftContractRatesMockSuccess } from './contractGQLMock' +export { fetchContractMockSuccess, fetchContractMockFail, updateDraftContractRatesMockSuccess, updateContractDraftRevisionMockFail, updateContractDraftRevisionMockSuccess, createContractMockFail, createContractMockSuccess } from './contractGQLMock' export { indexRatesMockSuccess, indexRatesMockFailure } from './rateGQLMocks' export { withdrawAndReplaceRedundantRateMock } from './replaceRateGQLMocks' diff --git a/services/cypress/integration/cmsWorkflow/submissionReview.spec.ts b/services/cypress/integration/cmsWorkflow/submissionReview.spec.ts index 9806d3ca9a..ae386fdcbc 100644 --- a/services/cypress/integration/cmsWorkflow/submissionReview.spec.ts +++ b/services/cypress/integration/cmsWorkflow/submissionReview.spec.ts @@ -8,7 +8,7 @@ describe('CMS user can view submission', () => { cy.logInAsStateUser() cy.startNewContractAndRatesSubmission() cy.fillOutBaseContractDetails() - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') cy.findByRole('heading', { level: 2, diff --git a/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts b/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts index 60c4ee81e8..62c0434e43 100644 --- a/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts +++ b/services/cypress/integration/cmsWorkflow/unlockResubmit.spec.ts @@ -13,7 +13,7 @@ describe('CMS user', () => { // fill out contract details cy.startNewContractAndRatesSubmission() cy.fillOutBaseContractDetails() - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') // fill out two child rates cy.findByRole('heading', { @@ -249,7 +249,7 @@ describe('CMS user', () => { cy.startNewContractAndRatesSubmission() cy.fillOutBaseContractDetails() - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') cy.findByRole('heading', { level: 2, name: /Rate details/ }) // Test unlock and resubmit with a linked rate submission diff --git a/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts b/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts index 0f4e4bb842..ed36dc4bd3 100644 --- a/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts +++ b/services/cypress/integration/stateWorkflow/stateSubmissionForm/rateDetails.spec.ts @@ -41,7 +41,7 @@ describe('rate details', () => { cy.fillOutBaseContractDetails() //Continue to Rate details page - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') cy.findByRole('heading', { level: 2, name: /Rate details/ }) //Add two more rate certifications, total three diff --git a/services/cypress/integration/stateWorkflow/stateSubmissionForm/submissionForm.spec.ts b/services/cypress/integration/stateWorkflow/stateSubmissionForm/submissionForm.spec.ts index 8a2ca7e680..647235e5c8 100644 --- a/services/cypress/integration/stateWorkflow/stateSubmissionForm/submissionForm.spec.ts +++ b/services/cypress/integration/stateWorkflow/stateSubmissionForm/submissionForm.spec.ts @@ -9,7 +9,7 @@ describe('state user in state submission form', () => { cy.logInAsStateUser() // Start a base contract only submissions - cy.startNewContractOnlySubmissionWithBaseContract() + cy.startNewContractOnlySubmissionWithBaseContractV2() // Save submission URL cy.location().then((fullUrl) => { @@ -44,18 +44,20 @@ describe('state user in state submission form', () => { ) // Change to contract and rates and contract amendment - cy.findByText('Contract action and rate certification').click() + cy.findByLabelText('Contract action and rate certification').check({force: true}) cy.findByLabelText('Contract action and rate certification').should( 'be.checked' ) - cy.findByText('Amendment to base contract').click() + cy.findByLabelText('Amendment to base contract').check({force: true}) + cy.findByLabelText('Amendment to base contract').should('be.checked') + cy.get('label[for="riskBasedContractNo"]').click() cy.findByRole('textbox', { name: 'Submission description' }).clear().type( 'description of contract only submission with amendment' ) // Save as draft - cy.deprecatedNavigateV1Form('SAVE_DRAFT') + cy.navigateContractForm('SAVE_DRAFT') cy.findByRole('heading', { level: 1, name: /Submissions dashboard/ }) // Link to type page and continue forward @@ -64,27 +66,27 @@ describe('state user in state submission form', () => { ) cy.findByTestId('step-indicator').findAllByRole('listitem').should('have.length', 6) cy.findByText('Rate details').should('exist') - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') // CHECK CONTRACT DETAILS PAGE NAVIGATION cy.findByRole('heading', { level: 2, name: /Contract details/ }) // Navigate back to previous page - cy.deprecatedNavigateV1Form('BACK') + cy.navigateContractForm('BACK') cy.findByRole('heading', { level: 2, name: /Submission type/ }) - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') // Change to contract amendment, save as draft cy.findByRole('heading', { level: 2, name: /Contract details/ }) cy.fillOutAmendmentToBaseContractDetails() - cy.deprecatedNavigateV1Form('SAVE_DRAFT') + cy.navigateContractForm('SAVE_DRAFT') cy.findByRole('heading', { level: 1, name: /Submissions dashboard/ }) // Link to contract details page and continue cy.navigateFormByDirectLink( `/submissions/${draftSubmissionId}/edit/contract-details` ) - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') // CHECK RATE DETAILS PAGE NAVIGATION cy.findByRole('heading', { level: 2, name: /Rate details/ }) @@ -92,7 +94,7 @@ describe('state user in state submission form', () => { // Navigate back to previous page cy.navigateContractRatesForm('BACK') cy.findByRole('heading', { level: 2, name: /Contract details/ }) - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') // Add base rate data, save as draft cy.findByRole('heading', { level: 2, name: /Rate details/ }) diff --git a/services/cypress/integration/stateWorkflow/submissionSummary.spec.ts b/services/cypress/integration/stateWorkflow/submissionSummary.spec.ts index 8ab06ac816..3cdfb4b317 100644 --- a/services/cypress/integration/stateWorkflow/submissionSummary.spec.ts +++ b/services/cypress/integration/stateWorkflow/submissionSummary.spec.ts @@ -9,13 +9,13 @@ describe('State user can view submissions', () => { // add a draft contract only submission cy.startNewContractOnlySubmissionWithBaseContract() - cy.deprecatedNavigateV1Form('SAVE_DRAFT') + cy.navigateContractForm('SAVE_DRAFT') // add a submitted contract and rates submission cy.startNewContractAndRatesSubmission() cy.fillOutBaseContractDetails() - cy.deprecatedNavigateV1Form('CONTINUE') + cy.navigateContractForm('CONTINUE') cy.findByRole('heading', { level: 2, diff --git a/services/cypress/support/commands.ts b/services/cypress/support/commands.ts index f82e1f6c44..faa536d6d5 100644 --- a/services/cypress/support/commands.ts +++ b/services/cypress/support/commands.ts @@ -53,12 +53,14 @@ Cypress.Commands.add('interceptGraphQL', () => { aliasQuery(req, 'indexRates') aliasQuery(req, 'fetchContract') aliasMutation(req, 'createHealthPlanPackage') + aliasMutation(req, 'createContract') aliasMutation(req, 'updateHealthPlanFormData') aliasMutation(req, 'submitHealthPlanPackage') aliasMutation(req, 'updateCMSUser') aliasMutation(req, 'createQuestion') aliasMutation(req, 'createQuestionResponse') aliasMutation(req, 'updateDraftContractRates') + aliasMutation(req, 'updateContractDraftRevision') aliasMutation(req, 'submitContract') }).as('GraphQL') }) diff --git a/services/cypress/support/index.ts b/services/cypress/support/index.ts index 57fecc8be4..574d29d326 100644 --- a/services/cypress/support/index.ts +++ b/services/cypress/support/index.ts @@ -52,6 +52,7 @@ declare global { // state submission form commands waitForDocumentsToLoad(): void startNewContractOnlySubmissionWithBaseContract(): void + startNewContractOnlySubmissionWithBaseContractV2(): void startNewContractOnlySubmissionWithAmendment(): void startNewContractAndRatesSubmission(): void fillOutContractActionOnlyWithBaseContract(): void @@ -80,6 +81,10 @@ declare global { buttonName: FormButtonKey, waitForLoad?: boolean ): void + navigateContractForm( + buttonName: FormButtonKey, + waitForLoad?: boolean + ): void navigateFormByDirectLink(url: string, waitForLoad?: boolean): void // dashboard commands diff --git a/services/cypress/support/navigateCommands.ts b/services/cypress/support/navigateCommands.ts index 5950ba783a..930fb41f54 100644 --- a/services/cypress/support/navigateCommands.ts +++ b/services/cypress/support/navigateCommands.ts @@ -30,7 +30,7 @@ Cypress.Commands.add( cy.findByRole('heading',{name:'Submissions'}).should('exist') } else if (buttonKey === 'CONTINUE_FROM_START_NEW') { if (waitForLoad) { - cy.wait('@createHealthPlanPackageMutation', { timeout: 50_000 }) + // cy.wait('@createHealthPlanPackageMutation', { timeout: 50_000 }) cy.wait('@fetchHealthPlanPackageWithQuestionsQuery') } cy.findByTestId('state-submission-form-page').should('exist') @@ -59,13 +59,13 @@ Cypress.Commands.add( if (buttonKey === 'SAVE_DRAFT') { if(waitForLoad) { cy.wait('@updateDraftContractRatesMutation', { timeout: 50_000}) - } + } cy.findByTestId('state-dashboard-page').should('exist') cy.findByRole('heading',{name:'Submissions'}).should('exist') } else if (buttonKey === 'CONTINUE_FROM_START_NEW') { if (waitForLoad) { // cy.wait('@createContractMutation', { timeout: 50_000 }) - cy.wait('@fetchContractQuery') + cy.wait('@fetchContractQuery', { timeout: 20_000 }) } cy.findByTestId('state-submission-form-page').should('exist') } else if (buttonKey === 'CONTINUE') { @@ -80,6 +80,41 @@ Cypress.Commands.add( } ) +// navigate helper for v2 forms +Cypress.Commands.add( + 'navigateContractForm', + (buttonKey: FormButtonKey, waitForLoad = true) => { + cy.findByRole('button', { + name: buttonsWithLabels[buttonKey], + }).should('not.have.attr', 'aria-disabled') + cy.findByRole('button', { + name: buttonsWithLabels[buttonKey], + }).safeClick() + + if (buttonKey === 'SAVE_DRAFT') { + if(waitForLoad) { + cy.wait('@updateContractDraftRevisionMutation', { timeout: 50_000}) + } + cy.findByTestId('state-dashboard-page').should('exist') + cy.findByRole('heading',{name:'Submissions'}).should('exist') + } else if (buttonKey === 'CONTINUE_FROM_START_NEW') { + if (waitForLoad) { + // cy.wait('@createContractMutation', { timeout: 50_000 }) + cy.wait('@fetchContractQuery', { timeout: 20_000 }) + } + cy.findByTestId('state-submission-form-page').should('exist') + } else if (buttonKey === 'CONTINUE') { + if (waitForLoad) { + cy.findAllByTestId('errorMessage').should('have.length', 0) + // cy.wait('@updateContractDraftRevisionMutation', { timeout: 50_000}) + } + cy.findByTestId('state-submission-form-page').should('exist') + } else { + cy.findByTestId('state-submission-form-page').should('exist') + } + } +) + Cypress.Commands.add( 'navigateFormByDirectLink', (url: string, waitForLoad = true) => { diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index 11724ba3da..98f260f3c3 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -10,6 +10,18 @@ Cypress.Commands.add('startNewContractOnlySubmissionWithBaseContract', () => { cy.findByRole('heading', { level: 2, name: /Contract details/ }) }) +Cypress.Commands.add('startNewContractOnlySubmissionWithBaseContractV2', () => { + // Must be on '/submissions/new' + cy.findByTestId('state-dashboard-page').should('exist') + cy.findByRole('link', { name: 'Start new submission' }).click() + cy.findByRole('heading', { level: 1, name: /New submission/ }) + + cy.fillOutContractActionOnlyWithBaseContract() + + cy.navigateContractForm('CONTINUE') + cy.findByRole('heading', { level: 2, name: /Contract details/ }) +}) + Cypress.Commands.add('startNewContractOnlySubmissionWithAmendment', () => { // Must be on '/submissions/new' cy.findByTestId('state-dashboard-page').should('exist')