From a659cd27d2bc845ac982ffa046ba0816371121a7 Mon Sep 17 00:00:00 2001 From: pearl-truss <67110378+pearl-truss@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:46:31 -0400 Subject: [PATCH 1/2] SubmissonSummaryV2 tests (#2370) * in progress submissonSummaryV2 tests * handle child rates correctly and get current data * update remaining test * update test following merge from main * fix tests --------- Co-authored-by: MacRae Linton --- .../src/gqlHelpers/contractsAndRates.ts | 53 +- .../pages/LinkYourRates/LinkRateSelect.tsx | 1 - .../EmailAnalystsTable.tsx | 72 +- .../EmailSettingsTables.tsx | 2 +- .../src/pages/Settings/Settings.module.scss | 4 +- .../RateDetails/V2/RateDetailsV2.test.tsx | 19 +- .../RateDetails/V2/RateDetailsV2.tsx | 6 +- .../RateDetails/V2/rateDetailsHelpers.ts | 4 +- .../ContractDetailsSummarySectionV2.test.tsx | 4 + .../ContractDetailsSummarySectionV2.tsx | 9 +- .../RateDetailsSummarySectionV2.test.tsx | 4 +- .../RateDetailsSummarySectionV2.tsx | 16 +- .../V2/ReviewSubmit/ReviewSubmitV2.test.tsx | 74 +- .../V2/ReviewSubmit/ReviewSubmitV2.tsx | 6 +- .../SubmissionTypeSummarySectionV2.test.tsx | 8 + .../SubmissionTypeSummarySectionV2.tsx | 6 +- .../V2/SubmissionSummaryV2.test.tsx | 812 ++++++++++++++++++ .../V2/SubmissionSummaryV2.tsx | 13 +- .../apolloMocks/contractPackageDataMock.ts | 450 ++++++---- 19 files changed, 1277 insertions(+), 286 deletions(-) create mode 100644 services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.test.tsx diff --git a/services/app-web/src/gqlHelpers/contractsAndRates.ts b/services/app-web/src/gqlHelpers/contractsAndRates.ts index 7f8c97b3e2..b7a2a7a08f 100644 --- a/services/app-web/src/gqlHelpers/contractsAndRates.ts +++ b/services/app-web/src/gqlHelpers/contractsAndRates.ts @@ -1,14 +1,57 @@ /* These helpers help you access nested data from the Contract and Rate Apollo Client types -If the data doesn't exist, returs undefined reliably +If the data doesn't exist, returns undefined reliably */ -import { Contract, ContractFormData, ContractPackageSubmission, Rate } from "../gen/gqlClient" +import { Contract, ContractFormData, ContractPackageSubmission, Rate, RateRevision } from "../gen/gqlClient" + + +function getVisibleLatestRateRevisions(contract: Contract, isEditing: boolean): RateRevision[] | undefined { + if (isEditing) { + if (!contract.draftRates) { + console.error('Programming Error: on the rate details page with no draft rates') + return undefined + } + const rateRevs = [] + for (const rate of contract.draftRates) { + // if this is a child rate, return draft revision + if (rate.parentContractID === contract.id) { + if (!rate.draftRevision) { + // TODO: this error will likely no longer apply once we have Unlock/Submit Rate + // child rates will no longer be assumed to be unlocked with their parent contracts + console.error('Programming Error: A child rate is not a draft') + return undefined + } + rateRevs.push(rate.draftRevision) + } else { + // otherwise return the latest revision submitted. + const lastRateSubmission = rate.revisions[0] + if (!lastRateSubmission) { + console.error('Programming Error: non-child rate was not previously submitted') + return undefined + } + rateRevs.push(lastRateSubmission) + } + } + return rateRevs + } else { + const lastContractSubmission = getLastContractSubmission(contract) + if (!lastContractSubmission) { + console.error('Programming Error: no contract submission for a contract were not editing') + return undefined + } + return lastContractSubmission.rateRevisions + } +} // returns draft form data for unlocked and draft, and last package submission data for submitted or resubmitted -const getLatestContractFormData = (contract: Contract): ContractFormData | undefined =>{ - return contract.draftRevision?.formData || +// only state users get to see draft data. +const getVisibleLatestContractFormData = (contract: Contract, isStateUser: boolean): ContractFormData | undefined =>{ + if (isStateUser) { + return contract.draftRevision?.formData || getLastContractSubmission(contract)?.contractRevision.formData + } + return getLastContractSubmission(contract)?.contractRevision.formData } const getLastContractSubmission = (contract: Contract): ContractPackageSubmission | undefined => { @@ -19,4 +62,4 @@ const getDraftRates = (contract: Contract): Rate[] | undefined => { return (contract.draftRates && contract.draftRates[0]) ? contract.draftRates : undefined } -export {getDraftRates, getLastContractSubmission, getLatestContractFormData} \ No newline at end of file +export {getDraftRates, getLastContractSubmission, getVisibleLatestContractFormData, getVisibleLatestRateRevisions} diff --git a/services/app-web/src/pages/LinkYourRates/LinkRateSelect.tsx b/services/app-web/src/pages/LinkYourRates/LinkRateSelect.tsx index 7ef571d3a2..423e8c8c73 100644 --- a/services/app-web/src/pages/LinkYourRates/LinkRateSelect.tsx +++ b/services/app-web/src/pages/LinkYourRates/LinkRateSelect.tsx @@ -113,7 +113,6 @@ export const LinkRateSelect = ({ ) => { if (action === 'select-option') { const linkedRateID = newValue.value - // const linkedRateName = newValue.label const linkedRate = rates.find((rate) => rate.id === linkedRateID) const linkedRateForm: FormikRateForm = convertGQLRateToRateForm( getKey, diff --git a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailAnalystsTable.tsx b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailAnalystsTable.tsx index e8cc85922e..3083a6c363 100644 --- a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailAnalystsTable.tsx +++ b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailAnalystsTable.tsx @@ -1,15 +1,26 @@ -import { Column, createColumnHelper, getCoreRowModel, getFacetedUniqueValues, getFilteredRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table" -import { StateAnalystsConfiguration } from "../../../gen/gqlClient" -import { useMemo, useRef } from "react" -import { FilterSelect, FilterSelectedOptionsType } from "../../../components/FilterAccordion" -import { DoubleColumnGrid } from "../../../components" -import { formatEmails } from "./EmailSettingsTables" -import { Table } from "@trussworks/react-uswds" +import { + Column, + createColumnHelper, + getCoreRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' +import { StateAnalystsConfiguration } from '../../../gen/gqlClient' +import { useMemo, useRef } from 'react' +import { + FilterSelect, + FilterSelectedOptionsType, +} from '../../../components/FilterAccordion' +import { DoubleColumnGrid } from '../../../components' +import { formatEmails } from './EmailSettingsTables' +import { Table } from '@trussworks/react-uswds' import styles from '../Settings.module.scss' -import { pluralize } from "../../../common-code/formatters" +import { pluralize } from '../../../common-code/formatters' -const columnHelper = createColumnHelper< StateAnalystsConfiguration>() +const columnHelper = createColumnHelper() const EmailAnalystsTable = ({ analysts, @@ -58,9 +69,9 @@ const EmailAnalystsTable = ({ 'emails' ) as Column const rowCount = `Displaying ${filteredRows.length} of ${analysts.length} ${pluralize( - 'analyst', - analysts.length - )}` + 'analyst', + analysts.length + )}` const updateFilters = ( column: Column, selectedOptions: FilterSelectedOptionsType, @@ -93,11 +104,7 @@ const EmailAnalystsTable = ({ label: state, }))} onChange={(selectedOptions) => - updateFilters( - stateColumn, - selectedOptions, - 'state' - ) + updateFilters(stateColumn, selectedOptions, 'state') } /> - updateFilters( - emailsColumn, - selectedOptions, - 'emails' - ) + updateFilters(emailsColumn, selectedOptions, 'emails') } /> -
- {rowCount} -
+
{rowCount}

@@ -135,19 +136,18 @@ const EmailAnalystsTable = ({ {filteredRows.map((row) => { - return ( - - - - - - ) - })} + return ( + + + + + ) + })}
{row.getValue('stateCode')} - {formatEmails(row.getValue('emails') || [])} -
{row.getValue('stateCode')} + {formatEmails(row.getValue('emails') || [])} +
) } -export {EmailAnalystsTable} \ No newline at end of file +export { EmailAnalystsTable } diff --git a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx index 3b5f4c727f..0b3e1e27d0 100644 --- a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx +++ b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx @@ -139,4 +139,4 @@ const EmailSupportTable = ({ config }: { config: EmailConfiguration }) => { ) } -export {EmailSettingsTable, formatEmails} \ No newline at end of file +export { EmailSettingsTable, formatEmails } diff --git a/services/app-web/src/pages/Settings/Settings.module.scss b/services/app-web/src/pages/Settings/Settings.module.scss index cc8422fcda..4ea8b10704 100644 --- a/services/app-web/src/pages/Settings/Settings.module.scss +++ b/services/app-web/src/pages/Settings/Settings.module.scss @@ -13,8 +13,8 @@ border-right: 0; } td { - word-wrap: break-word; - overflow-wrap: break-word; + word-wrap: break-word; + overflow-wrap: break-word; } } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx index a62ffb5d85..6b703e6b41 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx @@ -300,7 +300,7 @@ describe('RateDetailsv2', () => { }) it('display rest of the form when linked rates question is answered', async () => { - renderWithProviders( + const { user } = renderWithProviders( { contract: { ...mockContractWithLinkedRateDraft(), id: 'test-abc-123', - }, - }), - updateDraftContractRatesMockSuccess({ - contract: { - id: 'test-abc-123', + // clean draft rates for this test. + draftRates: [] }, }), ], @@ -340,16 +337,17 @@ describe('RateDetailsv2', () => { 'No, this rate certification was not included with any other submissions' ) ) + const input = screen.getByLabelText( 'Upload one rate certification document' ) - await expect(input).toBeInTheDocument() + expect(input).toBeInTheDocument() const submitButton = screen.getByRole('button', { name: 'Continue', }) // trigger validations - await submitButton.click() + await user.click(submitButton) await waitFor(() => { expect( screen.getByText('Rate certification 1') @@ -458,7 +456,7 @@ describe('RateDetailsv2', () => { }) }) it('cannot continue with partially filled out second rate', async () => { - renderWithProviders( + const { user } = renderWithProviders( { await screen.findByText('Rate Details') const rateCertsOnLoad = rateCertifications(screen) expect(rateCertsOnLoad).toHaveLength(1) - await fillOutIndexRate(screen, 0) await clickAddNewRate(screen) const submitButton = screen.getByRole('button', { @@ -503,7 +500,7 @@ describe('RateDetailsv2', () => { }) // trigger validations - await submitButton.click() + await user.click(submitButton) await waitFor(() => { expect( screen.getByText('Rate certification 1') diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx index 782e3dc8b8..4ad6961740 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -164,8 +164,8 @@ const RateDetailsV2 = ({ const [submitRate, { error: submitRateError }] = useSubmitRateMutation() // Set up data for form. Either based on contract API (for multi rate) or rates API (for edit and submit of standalone rate) - const ratesFromContract = - fetchContractData?.fetchContract.contract.draftRates + const contract = fetchContractData?.fetchContract.contract + const ratesFromContract = contract?.draftRates const initialRequestLoading = fetchContractLoading || fetchRateLoading const initialRequestError = fetchContractError || fetchRateError const submitRequestError = updateContractError || submitRateError @@ -188,7 +188,7 @@ const RateDetailsV2 = ({ rateForms: initialRates.length > 0 ? initialRates.map((rate) => - convertGQLRateToRateForm(getKey, rate) + convertGQLRateToRateForm(getKey, rate, contract?.id) ) : [convertGQLRateToRateForm(getKey)], } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts b/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts index 89d76e2493..dfcfb6f15a 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/rateDetailsHelpers.ts @@ -64,8 +64,8 @@ const convertRateFormToGQLRateFormData = ( // Convert from GQL Rate to FormikRateForm object used in the form // if rate is not passed in, return an empty RateForm // we need to pass in the s3 handler because 3 urls generated client-side // useLatestSubmission means to pull the latest submitted info rather than the draft info -const convertGQLRateToRateForm = (getKey: S3ClientT['getKey'], rate?: Rate): FormikRateForm => { - const handleAsLinkedRate = rate?.status && rate.status !== 'DRAFT' && rate.status !== 'UNLOCKED' // TODO: Make this a more sophisticated check for child-rates +const convertGQLRateToRateForm = (getKey: S3ClientT['getKey'], rate?: Rate, parentContractID?: string): FormikRateForm => { + const handleAsLinkedRate = rate && rate.parentContractID !== parentContractID // TODO: Make this a more sophisticated check for child-rates const rateRev = handleAsLinkedRate ? rate?.revisions[0] : rate?.draftRevision const rateForm = rateRev?.formData return { diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx index 01cd2b6966..7c1d1b09e9 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.test.tsx @@ -220,6 +220,7 @@ describe('ContractDetailsSummarySection', () => { , { apolloProvider: defaultApolloMocks, @@ -269,6 +270,7 @@ describe('ContractDetailsSummarySection', () => { , { apolloProvider: defaultApolloMocks, @@ -379,6 +381,7 @@ describe('ContractDetailsSummarySection', () => { , { apolloProvider: defaultApolloMocks, @@ -422,6 +425,7 @@ describe('ContractDetailsSummarySection', () => { , { apolloProvider: defaultApolloMocks, diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx index ddae446d85..d9f93d0bc7 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2.tsx @@ -45,7 +45,7 @@ import { SectionCard } from '../../../../../components/SectionCard' import { Contract } from '../../../../../gen/gqlClient' import { getLastContractSubmission, - getLatestContractFormData, + getVisibleLatestContractFormData, } from '../../../../../gqlHelpers/contractsAndRates' export type ContractDetailsSummarySectionV2Props = { @@ -84,8 +84,12 @@ export const ContractDetailsSummarySectionV2 = ({ string | undefined | Error >(undefined) const ldClient = useLDClient() + const isEditing = !isSubmitted(contract) && editNavigateTo !== undefined - const contractFormData = getLatestContractFormData(contract) + const contractFormData = getVisibleLatestContractFormData( + contract, + isEditing + ) const contract438Attestation = ldClient?.variation( featureFlags.CONTRACT_438_ATTESTATION.flag, featureFlags.CONTRACT_438_ATTESTATION.defaultValue @@ -96,7 +100,6 @@ export const ContractDetailsSummarySectionV2 = ({ booleanAsYesNoFormValue(contractFormData.statutoryRegulatoryAttestation) const contractSupportingDocuments = contractFormData?.supportingDocuments - const isEditing = !isSubmitted(contract) && editNavigateTo !== undefined const applicableFederalAuthorities = isCHIPOnly(contract) ? contractFormData?.federalAuthorities.filter((authority) => federalAuthorityKeysForCHIP.includes( diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx index 37cbee1f3b..51ff032cd0 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.test.tsx @@ -25,7 +25,7 @@ describe('RateDetailsSummarySection', () => { state: mockMNState(), stateCode: 'MN', stateNumber: 5, - parentContractID: 'fake-id', + parentContractID: 'test-abc-123', revisions: [], draftRevision: { id: '1234', @@ -74,7 +74,7 @@ describe('RateDetailsSummarySection', () => { state: mockMNState(), stateCode: 'MN', stateNumber: 5, - parentContractID: 'fake-id', + parentContractID: 'test-abc-123', revisions: [], draftRevision: { id: '1234', diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx index ef18776019..17947d20df 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2.tsx @@ -24,7 +24,10 @@ import { RateFormData, HealthPlanPackageStatus, } from '../../../../../gen/gqlClient' -import { getLastContractSubmission } from '../../../../../gqlHelpers/contractsAndRates' +import { + getLastContractSubmission, + getVisibleLatestRateRevisions, +} from '../../../../../gqlHelpers/contractsAndRates' export type RateDetailsSummarySectionV2Props = { contract: Contract @@ -74,12 +77,10 @@ export const RateDetailsSummarySectionV2 = ({ const isSubmitted = contract.status === 'SUBMITTED' const isEditing = !isSubmitted && editNavigateTo !== undefined const isPreviousSubmission = usePreviousSubmission() - const contractFormData = - contract.draftRevision?.formData || - getLastContractSubmission(contract)?.contractRevision.formData - const rates = isEditing - ? contract.draftRates - : getLastContractSubmission(contract)?.rateRevisions + const contractFormData = isEditing + ? contract.draftRevision?.formData + : getLastContractSubmission(contract)?.contractRevision.formData + const rates = getVisibleLatestRateRevisions(contract, isEditing) const lastSubmittedDate = getLastContractSubmission(contract)?.submitInfo.updatedAt ?? null @@ -224,6 +225,7 @@ export const RateDetailsSummarySectionV2 = ({ isSubmitted, isPreviousSubmission, ]) + return ( { it('renders without errors', async () => { @@ -204,33 +205,17 @@ describe('ReviewSubmit', () => { ) await waitFor(() => { - const contractDocRow = screen.getByRole('row', { - name: /contract document/, - }) - expect( - within(contractDocRow).getByText('1/1/24') - ).toBeInTheDocument() - const contractSupporting1Row = screen.getByRole('row', { - name: /contractSupporting1/, - }) - expect( - within(contractSupporting1Row).getByText('1/15/24') - ).toBeInTheDocument() - const rateDocRow = screen.getByRole('row', { - name: /rate certification/, - }) - expect(within(rateDocRow).getByText('1/13/24')).toBeInTheDocument() - const rateSupporting1Row = screen.getByRole('row', { - name: /rateSupporting1/, - }) - expect( - within(rateSupporting1Row).getByText('1/15/24') - ).toBeInTheDocument() + const rows = screen.getAllByRole('row') + expect(rows).toHaveLength(4) + expect(within(rows[0]).getByText('Date added')).toBeInTheDocument() + expect(within(rows[1]).getByText('2/2/23')).toBeInTheDocument() + expect(within(rows[2]).getByText('Date added')).toBeInTheDocument() + expect(within(rows[3]).getByText('3/2/23')).toBeInTheDocument() }) }) it('displays back, save as draft, and submit buttons', async () => { - renderWithProviders( + const { user } = renderWithProviders( { expect(screen.getByTestId('form-submit')).toBeDefined() expect(screen.getAllByText('Submit')).toHaveLength(2) - await screen.getAllByText('Submit')[0].click() + await user.click(screen.getAllByText('Submit')[0]) + }) + + it('pulls the right version of UNLOCKED data for state users', async () => { + renderWithProviders( + + } + /> + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidStateUser(), + }), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123/edit/review-and-submit', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + const description = await screen.findByLabelText( + 'Submission description' + ) + expect(description).toHaveTextContent('An updated submission') + const ratingPeriod = await screen.findByLabelText( + 'Rating period of original rate certification' + ) + expect(ratingPeriod).toHaveTextContent('02/02/2020 to 02/02/2021') }) }) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx index 06b8de606c..5e256c9d3d 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx @@ -15,7 +15,7 @@ import { STATE_SUBMISSION_FORM_ROUTES, } from '../../../../../constants' import { UnlockSubmitModalV2 } from '../../../../../components/Modal/V2/UnlockSubmitModalV2' -import { getLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' +import { getVisibleLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' import { useAuth } from '../../../../../contexts/AuthContext' import { RateDetailsSummarySectionV2 } from './RateDetailsSummarySectionV2' import { ContactsSummarySection } from './ContactsSummarySectionV2' @@ -75,7 +75,8 @@ export const ReviewSubmitV2 = (): React.ReactElement => { } } - const contractFormData = getLatestContractFormData(contract) + const isStateUser = loggedInUser?.role === 'STATE_USER' + const contractFormData = getVisibleLatestContractFormData(contract, isStateUser) if (!contractFormData) return const isContractActionAndRateCertification = @@ -113,6 +114,7 @@ export const ReviewSubmitV2 = (): React.ReactElement => { submissionName={submissionName} editNavigateTo="../type" statePrograms={statePrograms} + isStateUser={isStateUser} /> { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-PMAP-0001" + isStateUser={true} /> ) @@ -48,6 +49,7 @@ describe('SubmissionTypeSummarySection', () => { contract={stateSubmission} statePrograms={statePrograms} submissionName="MN-MSHO-0003" + isStateUser={true} /> ) @@ -71,6 +73,7 @@ describe('SubmissionTypeSummarySection', () => { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-PMAP-0001" + isStateUser={true} /> ) @@ -109,6 +112,7 @@ describe('SubmissionTypeSummarySection', () => { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-PMAP-0001" + isStateUser={true} /> ) } @@ -137,6 +141,7 @@ describe('SubmissionTypeSummarySection', () => { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-PMAP-0001" + isStateUser={true} /> ) } @@ -164,6 +169,7 @@ describe('SubmissionTypeSummarySection', () => { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-MSHO-0003" + isStateUser={true} /> ) expect( @@ -187,6 +193,7 @@ describe('SubmissionTypeSummarySection', () => { statePrograms={statePrograms} editNavigateTo="submission-type" submissionName="MN-PMAP-0001" + isStateUser={true} /> ) expect( @@ -202,6 +209,7 @@ describe('SubmissionTypeSummarySection', () => { editNavigateTo="submission-type" headerChildComponent={} submissionName="MN-PMAP-0001" + isStateUser={true} /> ) expect( diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx index bf31fc0259..83f7db7ba8 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx @@ -9,7 +9,7 @@ import { PopulationCoveredRecord, } from '../../../../../constants/healthPlanPackages' import { GenericErrorPage } from '../../../../Errors/GenericErrorPage' -import { getLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' +import { getVisibleLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' import { Program, Contract } from '../../../../../gen/gqlClient' import { usePreviousSubmission } from '../../../../../hooks/usePreviousSubmission' import { booleanAsYesNoUserValue } from '../../../../../components/Form/FieldYesNo/FieldYesNo' @@ -24,6 +24,7 @@ export type SubmissionTypeSummarySectionV2Props = { subHeaderComponent?: React.ReactElement initiallySubmittedAt?: Date submissionName: string + isStateUser: boolean } export const SubmissionTypeSummarySectionV2 = ({ @@ -34,9 +35,10 @@ export const SubmissionTypeSummarySectionV2 = ({ headerChildComponent, initiallySubmittedAt, submissionName, + isStateUser, }: SubmissionTypeSummarySectionV2Props): React.ReactElement => { const isPreviousSubmission = usePreviousSubmission() - const contractFormData = getLatestContractFormData(contract) + const contractFormData = getVisibleLatestContractFormData(contract, isStateUser) if (!contractFormData) return const programNames = statePrograms diff --git a/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.test.tsx b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.test.tsx new file mode 100644 index 0000000000..2406a842ab --- /dev/null +++ b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.test.tsx @@ -0,0 +1,812 @@ +import { screen, waitFor, within } from '@testing-library/react' +import { Route, Routes } from 'react-router' +import { RoutesRecord } from '../../../constants/routes' +import { + fetchCurrentUserMock, + fetchContractMockSuccess, + fetchStateHealthPlanPackageWithQuestionsMockSuccess, + mockValidCMSUser, + mockValidUser, + mockValidStateUser, + mockContractPackageSubmitted, +} from '../../../testHelpers/apolloMocks' +import { renderWithProviders } from '../../../testHelpers/jestHelpers' +import { SubmissionSummaryV2 } from './SubmissionSummaryV2' +import { SubmissionSideNav } from '../../SubmissionSideNav' +import { testS3Client } from '../../../testHelpers/s3Helpers' +import { mockContractPackageUnlocked } from '../../../testHelpers/apolloMocks/contractPackageDataMock' + +describe('SubmissionSummary', () => { + it('renders without errors', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + }), + fetchContractMockSuccess({ + contract: { + id: 'test-abc-123', + }, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + screen.debug() + expect( + await screen.findByRole('heading', { name: 'Contract details' }) + ).toBeInTheDocument() + }) + + it('renders submission updated banner', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + }), + fetchContractMockSuccess({ + contract: mockContractPackageSubmitted({ + status: 'RESUBMITTED', + }), + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByTestId('updatedSubmissionBanner') + ).toBeInTheDocument() + expect( + await screen.findByTestId('updatedSubmissionBanner') + ).toHaveClass('usa-alert--info') + expect( + await screen.findByTestId('updatedSubmissionBanner') + ).toHaveTextContent( + /Updated on: (0?[1-9]|[12][0-9]|3[01])\/[0-9]+\/[0-9]+\s[0-9]+:[0-9]+[a-zA-Z]+ ET/i + ) + expect( + await screen.findByTestId('updatedSubmissionBanner') + ).toHaveTextContent('Submitted by: example@state.com') + expect( + await screen.findByTestId('updatedSubmissionBanner') + ).toHaveTextContent('Changes made: contract submit') + }) + + it('renders submission unlocked banner for CMS user', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + expect(screen.getByTestId('unlockedBanner')).toBeInTheDocument() + expect(screen.getByTestId('unlockedBanner')).toHaveClass( + 'usa-alert--warning' + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + /on: (0?[1-9]|[12][0-9]|3[01])\/[0-9]+\/[0-9]+\s[0-9]+:[0-9]+[a-zA-Z]+ ET/i + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + 'by: example@state.com' + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + 'Reason for unlock: unlocked for a test' + ) + }) + }) + + it('pulls the right version of UNLOCKED data for CMS users', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect(await screen.findByText('MCR-MN-0005-SNBC')).toBeInTheDocument() + + const description = await screen.findByLabelText( + 'Submission description' + ) + expect(description).toHaveTextContent('An initial submission') + const ratingPeriod = await screen.findByLabelText( + 'Rating period of original rate certification' + ) + expect(ratingPeriod).toHaveTextContent('01/01/2020 to 01/01/2021') + }) + + it('renders add mccrs-id link for CMS user', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + fetchContractMockSuccess({ + contract: mockContractPackageSubmitted({ + id: 'test-abc-123', + }), + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + expect( + screen.getByText('Add MC-CRS record number') + ).toBeInTheDocument() + }) + }) + + it('renders edit mccrs-id link for CMS user when submission has a mccrs id', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchContractMockSuccess({ + contract: mockContractPackageSubmitted({ + id: 'test-abc-123', + mccrsID: '3333', + }), + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + expect( + screen.queryByText('Add MC-CRS record number') + ).not.toBeInTheDocument() + expect(screen.getByText('Edit MC-CRS number')).toBeInTheDocument() + }) + }) + + it('does not render an add mccrs-id link for state user', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidUser(), + statusCode: 200, + }), + fetchContractMockSuccess({ + contract: { + id: 'test-abc-123', + }, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + expect( + screen.queryByText('Add MC-CRS record number') + ).not.toBeInTheDocument() + }) + }) + + it('renders submission unlocked banner for State user', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + expect(screen.getByTestId('unlockedBanner')).toBeInTheDocument() + expect(screen.getByTestId('unlockedBanner')).toHaveClass( + 'usa-alert--info' + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + /on: (0?[1-9]|[12][0-9]|3[01])\/[0-9]+\/[0-9]+\s[0-9]+:[0-9]+[a-zA-Z]+ ET/i + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + 'by: example@state.com' + ) + expect(screen.getByTestId('unlockedBanner')).toHaveTextContent( + 'Reason for unlock: unlocked for a test' + ) + }) + }) + + it('renders document download warning banner', async () => { + const s3Provider = { + ...testS3Client(), + getBulkDlURL: async ( + _keys: string[], + _fileName: string + ): Promise => { + return new Error('Error: getBulkDlURL encountered an error') + }, + } + const contract = mockContractPackageSubmitted() + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract, + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + s3Provider, + } + ) + + await waitFor(() => { + expect(screen.getByTestId('warning-alert')).toBeInTheDocument() + expect(screen.getByTestId('warning-alert')).toHaveClass( + 'usa-alert--warning' + ) + expect(screen.getByTestId('warning-alert')).toHaveTextContent( + 'Document download unavailable' + ) + }) + }) + + it('renders back to dashboard link for state users', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract: { + id: 'test-abc-123', + }, + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + expect( + await screen.findByRole('heading', { + name: 'Contract details', + }) + ).toBeInTheDocument() + expect( + await screen.findByRole('link', { name: /Back to state dashboard/ }) + ).toBeInTheDocument() + }) + + it('renders back to dashboard link for CMS users', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByRole('link', { + name: /Back to dashboard/, + }) + ).toBeInTheDocument() + }) + + it('renders the sidenav for CMS users', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract: { + id: 'test-abc-123', + }, + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByTestId('submission-side-nav') + ).toBeInTheDocument() + }) + + it('renders the sidenav for State users', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidStateUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess({ + id: 'test-abc-123', + }), + fetchContractMockSuccess({ + contract: { + id: 'test-abc-123', + }, + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByTestId('submission-side-nav') + ).toBeInTheDocument() + }) + + describe('Submission package data display', () => { + it('renders the OLD data for an unlocked submission for CMS user, ignoring unsubmitted changes from state user', async () => { + const contract = mockContractPackageUnlocked() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + contract.draftRevision!.formData.submissionDescription = + 'NEW_DESCRIPTION' + contract.packageSubmissions[0].contractRevision.formData.submissionDescription = + 'OLD_DESCRIPTION' + + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess( + { + id: 'test-abc-123', + } + ), + fetchContractMockSuccess({ + contract, + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByText('OLD_DESCRIPTION') + ).toBeInTheDocument() + expect( + screen.queryByText('NEW_DESCRIPTION') + ).not.toBeInTheDocument() + }) + }) + + describe('CMS user unlock submission', () => { + it('renders the unlock button', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess( + { + id: 'test-abc-123', + } + ), + fetchContractMockSuccess({ + contract: mockContractPackageSubmitted(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + expect( + await screen.findByRole('button', { + name: 'Unlock submission', + }) + ).toBeInTheDocument() + }) + + it('extracts the correct dates from the submission and displays them in tables', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess( + { + id: 'test-abc-123', + } + ), + fetchContractMockSuccess({ + contract: mockContractPackageSubmitted(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + await waitFor(() => { + const rows = screen.getAllByRole('row') + expect(rows).toHaveLength(10) + expect( + within(rows[0]).getByText('Date added') + ).toBeInTheDocument() + expect(within(rows[1]).getByText('1/1/24')).toBeInTheDocument() + expect( + within(rows[2]).getByText('Date added') + ).toBeInTheDocument() + expect(within(rows[3]).getByText('1/15/24')).toBeInTheDocument() + expect(within(rows[4]).getByText('1/13/24')).toBeInTheDocument() + expect( + within(rows[5]).getByText('Date added') + ).toBeInTheDocument() + expect(within(rows[6]).getByText('1/1/23')).toBeInTheDocument() + expect( + within(rows[7]).getByText('Date added') + ).toBeInTheDocument() + expect(within(rows[8]).getByText('1/15/23')).toBeInTheDocument() + expect(within(rows[9]).getByText('1/15/23')).toBeInTheDocument() + }) + }) + + it('disables the unlock button for an unlocked submission', async () => { + renderWithProviders( + + }> + } + /> + + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + user: mockValidCMSUser(), + statusCode: 200, + }), + fetchStateHealthPlanPackageWithQuestionsMockSuccess( + { + id: 'test-abc-123', + } + ), + fetchContractMockSuccess({ + contract: mockContractPackageUnlocked(), + }), + ], + }, + routerProvider: { + route: '/submissions/test-abc-123', + }, + featureFlags: { + 'link-rates': true, + }, + } + ) + + await waitFor(() => { + expect( + screen.getByRole('button', { + name: 'Unlock submission', + }) + ).toBeDisabled() + }) + }) + }) +}) diff --git a/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx index 0bbcd1d6c5..70b69cd175 100644 --- a/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx +++ b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx @@ -34,6 +34,7 @@ import { useLDClient } from 'launchdarkly-react-client-sdk' import { featureFlags } from '../../../common-code/featureFlags' import { RoutesRecord } from '../../../constants' import { useRouteParams } from '../../../hooks' +import { getVisibleLatestContractFormData } from '../../../gqlHelpers/contractsAndRates' function UnlockModalButton({ disabled, @@ -115,11 +116,16 @@ export const SubmissionSummaryV2 = (): React.ReactElement => { } } const isCMSUser = loggedInUser?.role === 'CMS_USER' + const isStateUser = loggedInUser?.role === 'STATE_USER' const submissionStatus = contract.status const statePrograms = contract.state.programs - const contractFormData = - contract.draftRevision?.formData || - contract.packageSubmissions[0].contractRevision.formData + + const contractFormData = getVisibleLatestContractFormData(contract, isStateUser) + if (!contractFormData) { + console.error('no form data inside submission summary') + return + } + const programIDs = contractFormData.programIDs const programs = statePrograms.filter((program) => programIDs.includes(program.id) @@ -253,6 +259,7 @@ export const SubmissionSummaryV2 = (): React.ReactElement => { } statePrograms={statePrograms} initiallySubmittedAt={contract.initiallySubmittedAt} + isStateUser={isStateUser} /> )} diff --git a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts index 31fb04b4df..bb742f4bb8 100644 --- a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts @@ -15,6 +15,7 @@ function mockContractPackageDraft( stateCode: 'MN', state: mockMNState(), stateNumber: 5, + mccrsID: undefined, draftRevision: { __typename: 'ContractRevision', submitInfo: undefined, @@ -36,7 +37,7 @@ function mockContractPackageDraft( revisions: [], state: mockMNState(), stateNumber: 5, - parentContractID: 'foo-baz', + parentContractID: 'test-abc-123', draftRevision: { id: '123', rateID: '456', @@ -114,6 +115,7 @@ function mockContractWithLinkedRateDraft( stateCode: 'MN', state: mockMNState(), stateNumber: 5, + mccrsID: undefined, draftRevision: { __typename: 'ContractRevision', submitInfo: undefined, @@ -173,22 +175,24 @@ function mockContractWithLinkedRateDraft( }, draftRates: [ + // a linked, unlocked, rate. { - id: '123', + id: 'rate-123', createdAt: new Date(), updatedAt: new Date(), - status: 'SUBMITTED', + status: 'UNLOCKED', stateCode: 'MN', - revisions: [], state: mockMNState(), stateNumber: 5, - parentContractID: 'foo-baz', + parentContractID: 'some-other-contract-id', draftRevision: { id: '123', - rateID: '456', + rateID: 'rate-123', contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), + submitInfo: undefined, + unlockInfo: undefined, formData: { rateType: 'AMENDMENT', rateCapitationType: 'RATE_CELL', @@ -201,8 +205,8 @@ function mockContractWithLinkedRateDraft( }, ], supportingDocuments: [], - rateDateStart: new Date(), - rateDateEnd: new Date(), + rateDateStart: new Date('2020-02-02'), + rateDateEnd: new Date('2021-02-02'), rateDateCertified: new Date(), amendmentEffectiveDateStart: new Date(), amendmentEffectiveDateEnd: new Date(), @@ -226,115 +230,15 @@ function mockContractWithLinkedRateDraft( actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], } - } - - }, - ], - packageSubmissions: [], - ...partial, - } -} -function mockContractPackageUnlocked( - partial?: Partial -): Contract { - return { - __typename: 'Contract', - initiallySubmittedAt: undefined, - status: 'UNLOCKED', - createdAt: new Date('01/01/24'), - updatedAt: new Date(), - id: 'test-abc-123', - stateCode: 'MN', - state: mockMNState(), - stateNumber: 5, - draftRevision: { - __typename: 'ContractRevision', - submitInfo: undefined, - unlockInfo: undefined, - id: '123', - createdAt: new Date(), - updatedAt: new Date(), - contractName: 'MCR-0005-alvhalfhdsalf', - formData: { - programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_AND_RATES', - riskBasedContract: true, - submissionDescription: 'A real submission', - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/contractsupporting1', - sha256: 'fakesha', - name: 'contractSupporting1', - dateAdded: new Date('01/15/2024') - }, - { - s3URL: 's3://bucketname/key/contractSupporting2', - sha256: 'fakesha', - name: 'contractSupporting2', - dateAdded: new Date('01/13/2024') - }, - ], - 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: 'contract document', - dateAdded: new Date('01/01/2024') - }, - ], - contractDateStart: new Date('01/01/2023'), - contractDateEnd: new Date('12/31/2023'), - 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: 'DRAFT', - stateCode: 'MN', - revisions: [], - state: mockMNState(), - stateNumber: 5, - parentContractID:'test-abc-123', - draftRevision: { + }, + revisions: [{ id: '456', - rateID: '123', + rateID: 'rate-123', contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), + submitInfo: undefined, + unlockInfo: undefined, formData: { rateType: 'AMENDMENT', rateCapitationType: 'RATE_CELL', @@ -342,26 +246,13 @@ function mockContractPackageUnlocked( { s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', - name: 'rate certification', - dateAdded: new Date('01/13/2024') - }, - ], - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/ratesupporting1', - sha256: 'fakesha', - name: 'rateSupporting1', - dateAdded: new Date('01/15/2024') - }, - { - s3URL: 's3://bucketname/key/rateSupporting2', - sha256: 'fakesha', - name: 'rateSupporting2', - dateAdded: new Date('01/13/2024') + name: 'rate', + dateAdded: new Date() }, ], - rateDateStart: new Date(), - rateDateEnd: new Date(), + supportingDocuments: [], + rateDateStart: new Date('2020-01-01'), + rateDateEnd: new Date('2021-01-01'), rateDateCertified: new Date(), amendmentEffectiveDateStart: new Date(), amendmentEffectiveDateEnd: new Date(), @@ -385,23 +276,44 @@ function mockContractPackageUnlocked( actuaryCommunicationPreference: 'OACT_TO_ACTUARY', packagesWithSharedRateCerts: [], } - } - + }] }, ], + packageSubmissions: [], + ...partial, + } +} + +function mockContractPackageSubmitted( + partial?: Partial +): Contract { + return { + status: 'SUBMITTED', + createdAt: new Date(), + updatedAt: new Date(), + id: 'test-abc-123', + stateCode: 'MN', + state: mockMNState(), + stateNumber: 5, packageSubmissions: [{ cause: 'CONTRACT_SUBMISSION', submitInfo: { - updatedAt: new Date('12/31/2023'), + updatedAt: new Date(), updatedBy: 'example@state.com', updatedReason: 'contract submit' }, submittedRevisions: [], contractRevision: { contractName: 'MCR-MN-0005-SNBC', - createdAt: new Date('01/01/2023'), - updatedAt: new Date('12/31/2023'), + createdAt: new Date('01/01/2024'), + updatedAt: new Date('12/31/2024'), id: '123', + submitInfo: { + updatedAt: new Date(), + updatedBy: 'example@state.com', + updatedReason: 'contract submit' + }, + unlockInfo: undefined, formData: { programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], populationCovered: 'MEDICAID', @@ -413,13 +325,13 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/contractsupporting1', sha256: 'fakesha', name: 'contractSupporting1', - dateAdded: new Date('01/15/2023') + dateAdded: new Date('01/15/2024') }, { s3URL: 's3://bucketname/key/contractSupporting2', sha256: 'fakesha', name: 'contractSupporting2', - dateAdded: new Date('01/13/2023') + dateAdded: new Date('01/13/2024') }, ], stateContacts: [], @@ -430,7 +342,7 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - dateAdded: new Date('01/01/2023') + dateAdded: new Date('01/01/2024') }, ], contractDateStart: new Date(), @@ -461,7 +373,7 @@ function mockContractPackageUnlocked( rateRevisions: [ { id: '1234', - rateID: '456', + rateID: '123', createdAt: new Date('01/01/2023'), updatedAt: new Date('01/01/2023'), contractRevisions: [], @@ -521,50 +433,234 @@ function mockContractPackageUnlocked( ...partial, } } -function mockContractPackageSubmitted( + +function mockContractPackageUnlocked( partial?: Partial ): Contract { return { - status: 'SUBMITTED', + status: 'UNLOCKED', createdAt: new Date(), updatedAt: new Date(), + initiallySubmittedAt: new Date(), id: 'test-abc-123', stateCode: 'MN', state: mockMNState(), stateNumber: 5, + mccrsID: '1234', + draftRevision: { + __typename: 'ContractRevision', + submitInfo: undefined, + unlockInfo: { + updatedAt: new Date(), + updatedBy: 'soandso@example.com', + 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: 'soandso@example.com', + 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'], + 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: new Date(), + updatedAt: new Date('01/01/2024'), updatedBy: 'example@state.com', - updatedReason: 'contract submit' + updatedReason: 'initial submission' }, - submittedRevisions: [], + submittedRevisions: [ + { + contractName: 'MCR-MN-0005-SNBC', + createdAt: new Date('01/01/2024'), + updatedAt: new Date('12/31/2024'), + submitInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: 'example@state.com', + updatedReason: 'initial submission' + }, + unlockInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: 'example@state.com', + 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: new Date('12/31/2024'), + submitInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: 'example@state.com', + updatedReason: 'initial submission' + }, + unlockInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: 'example@state.com', + updatedReason: 'unlocked' + }, id: '123', formData: { programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], populationCovered: 'MEDICAID', submissionType: 'CONTRACT_AND_RATES', riskBasedContract: true, - submissionDescription: 'A real submission', - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/contractsupporting1', - sha256: 'fakesha', - name: 'contractSupporting1', - dateAdded: new Date('01/15/2024') - }, - { - s3URL: 's3://bucketname/key/contractSupporting2', - sha256: 'fakesha', - name: 'contractSupporting2', - dateAdded: new Date('01/13/2024') - }, - ], + submissionDescription: 'An initial submission', + supportingDocuments: [], stateContacts: [], contractType: 'AMENDMENT', contractExecutionStatus: 'EXECUTED', @@ -573,11 +669,11 @@ function mockContractPackageSubmitted( s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - dateAdded: new Date('01/01/2024') + dateAdded: new Date() }, ], - contractDateStart: new Date(), - contractDateEnd: new Date(), + contractDateStart: new Date('01/01/2023'), + contractDateEnd: new Date('01/01/2024'), managedCareEntities: ['MCO'], federalAuthorities: ['STATE_PLAN'], inLieuServicesAndSettings: true, @@ -604,9 +700,14 @@ function mockContractPackageSubmitted( rateRevisions: [ { id: '1234', - rateID: '123', + rateID: '456', createdAt: new Date('01/01/2023'), updatedAt: new Date('01/01/2023'), + submitInfo: { + updatedAt: new Date('01/01/2024'), + updatedBy: 'example@state.com', + updatedReason: 'initial submission' + }, contractRevisions: [], formData: { rateType: 'AMENDMENT', @@ -616,25 +717,12 @@ function mockContractPackageSubmitted( s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate', - dateAdded: new Date('01/01/2023') - }, - ], - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/rateSupporting1', - sha256: 'fakesha', - name: 'rate supporting 1', - dateAdded: new Date('01/15/2023') - }, - { - s3URL: 's3://bucketname/key/rateSupporting1', - sha256: 'fakesha', - name: 'rate supporting 2', - dateAdded: new Date('01/15/2023') + dateAdded: new Date() }, ], - rateDateStart: new Date(), - rateDateEnd: new Date(), + supportingDocuments: [], + rateDateStart: new Date('2020-01-01'), + rateDateEnd: new Date('2021-01-01'), rateDateCertified: new Date(), amendmentEffectiveDateStart: new Date(), amendmentEffectiveDateEnd: new Date(), From 254a6c7c0a66d34057cba5d6c8e5ef93aa3390d4 Mon Sep 17 00:00:00 2001 From: MacRae Linton <55759+macrael@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:20:54 -0700 Subject: [PATCH 2/2] Link Rate FF for unlock contract (#2377) --- .../contractAndRates/unlockContract.ts | 111 ++++++++++-------- .../postgres/contractAndRates/unlockRate.ts | 41 ++++--- .../app-api/src/postgres/postgresStore.ts | 6 +- .../src/resolvers/configureResolvers.ts | 3 +- .../resolvers/contract/submitContract.test.ts | 30 ++++- .../unlockHealthPlanPackage.ts | 19 ++- 6 files changed, 135 insertions(+), 75 deletions(-) diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index 1c123f1c03..e00ee2f5f0 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -16,33 +16,13 @@ type UnlockContractArgsType = { // * set relationships based on last submission async function unlockContract( client: PrismaClient, - args: UnlockContractArgsType + args: UnlockContractArgsType, + linkRatesFF?: boolean ): Promise { const { contractID, unlockedByUserID, unlockReason } = args try { return await client.$transaction(async (tx) => { - // This finds the child rates for this submission. - // A child rate is a rate that shares a submit info with this contract. - // technically only a rate that is _initially_ submitted with a contract - // is a child rate, but we should never allow re-submission so this simpler - // query that doesn't try to filter to initial revisions works. - const childRates = await tx.rateTable.findMany({ - where: { - revisions: { - some: { - submitInfo: { - submittedContracts: { - some: { - contractID: contractID, - }, - }, - }, - }, - }, - }, - }) - const currentDateTime = new Date() // create the unlock info to be shared across all submissions. const unlockInfo = await tx.updateInfoTable.create({ @@ -53,18 +33,6 @@ async function unlockContract( }, }) - // unlock child rates with that unlock info - for (const childRate of childRates) { - const unlockRate = await unlockRateInDB( - tx, - childRate.id, - unlockInfo.id - ) - if (unlockRate instanceof Error) { - throw unlockRate - } - } - // get the last submitted rev in order to unlock it const currentRev = await tx.contractRevisionTable.findFirst({ where: { @@ -132,19 +100,70 @@ async function unlockContract( ) } - const previouslySubmittedRateIDs = currentRev.rateRevisions.map( - (c) => c.rateRevision.rateID - ) + const childRateIDs: string[] = [] + if (linkRatesFF) { + // This finds the child rates for this submission. + // A child rate is a rate that shares a submit info with this contract. + // technically only a rate that is _initially_ submitted with a contract + // is a child rate, but we should never allow re-submission so this simpler + // query that doesn't try to filter to initial revisions works. + const childRates = await tx.rateTable.findMany({ + where: { + revisions: { + some: { + submitInfo: { + submittedContracts: { + some: { + contractID: contractID, + }, + }, + }, + }, + }, + }, + }) + + for (const childRate of childRates) { + childRateIDs.push(childRate.id) + } + } else { + // without linked rates, we push all the valid rate revisions attached to the contract revision + for (const rrev of currentRev.rateRevisions) { + childRateIDs.push(rrev.rateRevision.rateID) + } + } - // find the rates in the last submission package: - const lastSubmission = currentRev.relatedSubmisions[0] - const thisContractsRatePackages = - lastSubmission.submissionPackages.filter( - (p) => p.contractRevisionID === currentRev.id + // unlock child rates with that unlock info + for (const childRateID of childRateIDs) { + const unlockRate = await unlockRateInDB( + tx, + childRateID, + unlockInfo.id, + linkRatesFF ) - const relatedRateIDs = thisContractsRatePackages.map( - (p) => p.rateRevision.rateID - ) + if (unlockRate instanceof Error) { + throw unlockRate + } + } + + const relatedRateIDs: string[] = [] + if (linkRatesFF) { + // find the rates in the last submission package: + const lastSubmission = currentRev.relatedSubmisions[0] + const thisContractsRatePackages = + lastSubmission.submissionPackages.filter( + (p) => p.contractRevisionID === currentRev.id + ) + + for (const ratePackage of thisContractsRatePackages) { + relatedRateIDs.push(ratePackage.rateRevision.rateID) + } + } else { + for (const rateRev of currentRev.rateRevisions) { + relatedRateIDs.push(rateRev.rateRevision.rateID) + } + } + await tx.contractRevisionTable.create({ data: { contract: { @@ -156,7 +175,7 @@ async function unlockContract( connect: { id: unlockInfo.id }, }, draftRates: { - connect: previouslySubmittedRateIDs.map((cID) => ({ + connect: relatedRateIDs.map((cID) => ({ id: cID, })), }, diff --git a/services/app-api/src/postgres/contractAndRates/unlockRate.ts b/services/app-api/src/postgres/contractAndRates/unlockRate.ts index 703c5320c5..d1758f50fb 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockRate.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockRate.ts @@ -13,7 +13,8 @@ type UnlockRateArgsType = { async function unlockRateInDB( tx: PrismaTransactionType, rateID: string, - unlockInfoID: string + unlockInfoID: string, + linkRatesFF?: boolean ): Promise { // find the current rate revision in order to create a new unlocked revision const currentRev = await tx.rateRevisionTable.findFirst({ @@ -87,6 +88,8 @@ async function unlockRateInDB( ) } + // old way to find connected contracts + //TODO: with linked rates on, find them via package submission const previouslySubmittedContractIDs = currentRev.contractRevisions.map( (c) => c.contractRevision.contractID ) @@ -177,25 +180,29 @@ async function unlockRateInDB( }, }) - // add DraftContract connections to the Rate - const lastSubmission = currentRev.relatedSubmissions[0] - const submissionConnections = lastSubmission.submissionPackages.filter( - (p) => p.rateRevisionID === currentRev.id - ) - const newDraftConnections = [] - for (const submissionConnection of submissionConnections) { - newDraftConnections.push({ - contractID: submissionConnection.contractRevision.contractID, - rateID: currentRev.rateID, - ratePosition: submissionConnection.ratePosition, + // Unlock rate will only ever by run after the linked rates FF is enabled + // we only need to set draft rates explicitly in that case, when doing a rate + // unlock UNDER a contract unlock, the contract will set the draft rates correctly. + if (linkRatesFF) { + // add DraftContract connections to the Rate + const lastSubmission = currentRev.relatedSubmissions[0] + const submissionConnections = lastSubmission.submissionPackages.filter( + (p) => p.rateRevisionID === currentRev.id + ) + const newDraftConnections = [] + for (const submissionConnection of submissionConnections) { + newDraftConnections.push({ + contractID: submissionConnection.contractRevision.contractID, + rateID: currentRev.rateID, + ratePosition: submissionConnection.ratePosition, + }) + } + await tx.draftRateJoinTable.createMany({ + data: newDraftConnections, + skipDuplicates: true, }) } - await tx.draftRateJoinTable.createMany({ - data: newDraftConnections, - skipDuplicates: true, - }) - return currentRev.id } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 3306b86c6a..d56524ecff 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -134,7 +134,8 @@ type Store = { submitRate: (args: SubmitRateArgsType) => Promise unlockContract: ( - args: UnlockContractArgsType + args: UnlockContractArgsType, + linkRatesFF?: boolean ) => Promise unlockRate: (args: UnlockRateArgsType) => Promise @@ -189,7 +190,8 @@ function NewPostgresStore(client: PrismaClient): Store { findAllRatesWithHistoryBySubmitInfo(client), submitContract: (args) => submitContract(client, args), submitRate: (args) => submitRate(client, args), - unlockContract: (args) => unlockContract(client, args), + unlockContract: (args, linkRatesFF) => + unlockContract(client, args, linkRatesFF), unlockRate: (args) => unlockRate(client, args), } } diff --git a/services/app-api/src/resolvers/configureResolvers.ts b/services/app-api/src/resolvers/configureResolvers.ts index 2569daaf51..d8a25cacfe 100644 --- a/services/app-api/src/resolvers/configureResolvers.ts +++ b/services/app-api/src/resolvers/configureResolvers.ts @@ -88,7 +88,8 @@ export function configureResolvers( unlockHealthPlanPackage: unlockHealthPlanPackageResolver( store, emailer, - emailParameterStore + emailParameterStore, + launchDarkly ), updateContract: updateContract(store), updateDraftContractRates: updateDraftContractRates(store), diff --git a/services/app-api/src/resolvers/contract/submitContract.test.ts b/services/app-api/src/resolvers/contract/submitContract.test.ts index baf9d74ee0..85c98711df 100644 --- a/services/app-api/src/resolvers/contract/submitContract.test.ts +++ b/services/app-api/src/resolvers/contract/submitContract.test.ts @@ -646,11 +646,19 @@ describe('submitContract', () => { }) it('handles unlock and editing rates', async () => { - const stateServer = await constructTestPostgresServer() + const ldService = testLDService({ + 'link-rates': true, + 'rate-edit-unlock': true, + }) + const stateServer = await constructTestPostgresServer({ + ldService, + }) + const cmsServer = await constructTestPostgresServer({ context: { user: testCMSUser(), }, + ldService, }) console.info('1.') @@ -724,11 +732,19 @@ describe('submitContract', () => { }) it('checks parent rates on update', async () => { - const stateServer = await constructTestPostgresServer() + const ldService = testLDService({ + 'link-rates': true, + 'rate-edit-unlock': true, + }) + const stateServer = await constructTestPostgresServer({ + ldService, + }) + const cmsServer = await constructTestPostgresServer({ context: { user: testCMSUser(), }, + ldService, }) console.info('1.') @@ -833,11 +849,19 @@ describe('submitContract', () => { it('can remove a child unlocked rate', async () => { //TODO: make a child rate, submit and unlock, then remove it. - const stateServer = await constructTestPostgresServer() + const ldService = testLDService({ + 'link-rates': true, + 'rate-edit-unlock': true, + }) + const stateServer = await constructTestPostgresServer({ + ldService, + }) + const cmsServer = await constructTestPostgresServer({ context: { user: testCMSUser(), }, + ldService, }) console.info('1.') diff --git a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts index 64d5624144..c3a5fcb4b6 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts @@ -19,18 +19,22 @@ import { } from '../attributeHelper' import type { EmailParameterStore } from '../../parameterStore' import { GraphQLError } from 'graphql' +import type { LDService } from '../../launchDarkly/launchDarkly' // unlockHealthPlanPackageResolver is a state machine transition for HealthPlanPackage export function unlockHealthPlanPackageResolver( store: Store, emailer: Emailer, - emailParameterStore: EmailParameterStore + emailParameterStore: EmailParameterStore, + launchDarkly: LDService ): MutationResolvers['unlockHealthPlanPackage'] { return async (_parent, { input }, context) => { const { user, span } = context const { unlockedReason, pkgID } = input setResolverDetailsOnActiveSpan('unlockHealthPlanPackage', user, span) span?.setAttribute('mcreview.package_id', pkgID) + const featureFlags = await launchDarkly.allFlags(context) + const linkRatesFF = featureFlags?.['link-rates'] === true // This resolver is only callable by CMS users if (!isCMSUser(user)) { @@ -81,11 +85,14 @@ export function unlockHealthPlanPackageResolver( } // Now, unlock the contract! - const unlockContractResult = await store.unlockContract({ - contractID: contract.id, - unlockReason: unlockedReason, - unlockedByUserID: user.id, - }) + const unlockContractResult = await store.unlockContract( + { + contractID: contract.id, + unlockReason: unlockedReason, + unlockedByUserID: user.id, + }, + linkRatesFF + ) if (unlockContractResult instanceof Error) { const errMessage = `Failed to unlock contract revision with ID: ${contract.id}; ${unlockContractResult.message}` logError('unlockHealthPlanPackage', errMessage)