diff --git a/packages/mocks/src/apollo/contractPackageDataMock.ts b/packages/mocks/src/apollo/contractPackageDataMock.ts index 08da819027..362e8759cc 100644 --- a/packages/mocks/src/apollo/contractPackageDataMock.ts +++ b/packages/mocks/src/apollo/contractPackageDataMock.ts @@ -7,6 +7,7 @@ import { UnlockedContract, CmsUser, StateUser, + Rate, } from '../gen/gqlClient' import { s3DlUrl } from './documentDataMock' @@ -2833,6 +2834,95 @@ const mockEmptyDraftContractAndRate = (): Contract => withdrawnRates: [], }) +const mockWithdrawnRates = (parentContractID?: string): Rate[] => { + return [ + { + id: '1234', + webURL: 'https://testmcreview.example/rates/1234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + status: 'SUBMITTED', + reviewStatus: 'WITHDRAWN', + consolidatedStatus: 'WITHDRAWN', + state: mockMNState(), + stateCode: 'MN', + stateNumber: 5, + parentContractID: parentContractID ?? 'test-abc-123', + revisions: [], + packageSubmissions: [ + { + cause: 'CONTRACT_SUBMISSION', + submitInfo: { + updatedAt: new Date('01/01/2021'), + updatedBy: { + email: 'testCMS@example.com', + familyName: 'Hotman', + givenName: 'Zuko', + role: 'CMS_USER', + }, + updatedReason: 'Withdrawn rate reason', + }, + contractRevisions: [], + rateRevision: { + id: 'test-rate-revision-id', + rateID: '1234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + formData: { + ...mockRateRevision().formData, + rateCertificationName: + 'WITHDRAWN-RATE-1-NAME' + }, + }, + submittedRevisions: [], + }, + ], + }, + { + id: '5678', + webURL: 'https://testmcreview.example/rates/5678', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + status: 'SUBMITTED', + reviewStatus: 'WITHDRAWN', + consolidatedStatus: 'WITHDRAWN', + state: mockMNState(), + stateCode: 'MN', + stateNumber: 5, + parentContractID: parentContractID ?? 'test-abc-123', + revisions: [], + packageSubmissions: [ + { + cause: 'CONTRACT_SUBMISSION', + submitInfo: { + updatedAt: new Date('01/01/2021'), + updatedBy: { + email: 'testCMS@example.com', + familyName: 'Hotman', + givenName: 'Zuko', + role: 'CMS_USER', + }, + updatedReason: 'Withdrawn rate reason', + }, + contractRevisions: [], + rateRevision: { + id: 'test-rate-revision-id', + rateID: '5678', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('01/01/2021'), + formData: { + ...mockRateRevision().formData, + rateCertificationName: + 'WITHDRAWN-RATE-2-NAME' + }, + }, + submittedRevisions: [], + }, + ], + }, + ] +} + export { mockContractRevision, mockContractPackageDraft, @@ -2848,4 +2938,5 @@ export { mockContractPackageSubmittedWithQuestions, mockContractPackageApproved, mockContractPackageApprovedWithQuestions, + mockWithdrawnRates } diff --git a/services/app-web/src/components/InfoTag/InfoTag.module.scss b/services/app-web/src/components/InfoTag/InfoTag.module.scss index a6629e70d9..01866f758b 100644 --- a/services/app-web/src/components/InfoTag/InfoTag.module.scss +++ b/services/app-web/src/components/InfoTag/InfoTag.module.scss @@ -13,6 +13,11 @@ color: custom.$mcr-foundation-ink; } +.gray-medium { + @include uswds.u-bg('gray-60'); + color: custom.$mcr-foundation-white; +} + .light-green { @include uswds.u-bg('green-cool-20v'); color: custom.$mcr-foundation-ink; diff --git a/services/app-web/src/components/InfoTag/InfoTag.tsx b/services/app-web/src/components/InfoTag/InfoTag.tsx index 6a5402eb33..62f686584d 100644 --- a/services/app-web/src/components/InfoTag/InfoTag.tsx +++ b/services/app-web/src/components/InfoTag/InfoTag.tsx @@ -9,7 +9,14 @@ Main application-wide tag to draw attention to key info. This is a react-uswds Tag enhanced with CMS styles. */ export type TagProps = { - color: 'green' | 'gold' | 'cyan' | 'blue' | 'light green' | 'gray' + color: + | 'green' + | 'gold' + | 'cyan' + | 'blue' + | 'light green' + | 'gray' + | 'gray-medium' emphasize?: boolean } & ComponentProps @@ -28,6 +35,7 @@ export const InfoTag = ({ [styles['gold']]: color === 'gold', [styles['blue']]: color === 'blue', [styles['gray']]: color === 'gray', + [styles['gray-medium']]: color === 'gray-medium', }, emphasize ? styles['emphasize'] : undefined, className diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx index 40d7b7f4c8..980f86c622 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx @@ -13,19 +13,16 @@ import { updateDraftContractRatesMockSuccess, mockContractWithLinkedRateDraft, mockContractPackageDraft, -} from '@mc-review/mocks' -import { Route, Routes, Location } from 'react-router-dom' -import { RoutesRecord } from '@mc-review/constants' -import userEvent from '@testing-library/user-event' -import { rateDataMock, rateRevisionDataMock, draftRateDataMock, -} from '@mc-review/mocks' -import { fetchDraftRateMockSuccess, indexRatesMockSuccess, + mockWithdrawnRates, } from '@mc-review/mocks' +import { Route, Routes, Location } from 'react-router-dom' +import { RoutesRecord } from '@mc-review/constants' +import userEvent from '@testing-library/user-event' import { clickAddNewRate, fillOutFirstRate, @@ -1674,4 +1671,59 @@ describe('RateDetails', () => { expect(removeButtonsPostRemoval).toHaveLength(2) }) }) + + describe('handles withdrawn rates', () => { + const withdrawnRates = mockWithdrawnRates() + it('renders withdrawn rates', async () => { + renderWithProviders( + + } + /> + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ statusCode: 200 }), + fetchContractMockSuccess({ + contract: { + ...mockContractWithLinkedRateDraft(), + id: 'test-abc-123', + withdrawnRates, + }, + }), + ], + }, + routerProvider: { + route: `/submissions/test-abc-123/edit/rate-details`, + }, + featureFlags: { + 'rate-edit-unlock': false, + }, + } + ) + + await screen.findByText('Rate Details') + expect( + screen.getByText( + 'Was this rate certification included with another submission?' + ) + ).toBeInTheDocument() + + // expect withdrawn rates to be on the screen + expect( + screen.getByRole('heading', { + level: 3, + name: /WITHDRAWN-RATE-1-NAME/, + }) + ).toBeInTheDocument() + expect( + screen.getByRole('heading', { + level: 3, + name: /WITHDRAWN-RATE-2-NAME/, + }) + ).toBeInTheDocument() + }) + }) }) 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 4015fd7ac7..b2240596ba 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -54,6 +54,7 @@ import { import { LinkYourRates } from '../../../LinkYourRates/LinkYourRates' import { LinkedRateSummary } from '../LinkedRateSummary' import { usePage } from '../../../../contexts/PageContext' +import { InfoTag } from '../../../../components/InfoTag/InfoTag' export type FormikRateForm = { id?: string // no id if its a new rate @@ -164,20 +165,28 @@ const RateDetails = ({ } }, [focusNewRate]) - const pageHeading = displayAsStandaloneRate - ? fetchRateData?.fetchRate.rate.draftRevision?.formData - .rateCertificationName - : fetchContractData?.fetchContract.contract?.draftRevision?.contractName - if (pageHeading) updateHeading({ customHeading: pageHeading }) - const [updateDraftContractRates] = useUpdateDraftContractRatesMutation() - const [submitRate] = 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 contract = fetchContractData?.fetchContract.contract const contractDraftRevision = contract?.draftRevision const ratesFromContract = contract?.draftRates const initialRequestLoading = fetchContractLoading || fetchRateLoading const initialRequestError = fetchContractError || fetchRateError + const withdrawnRateRevisions: RateRevision[] = + contract?.withdrawnRates?.reduce((acc, rate) => { + const latestRevision = rate.packageSubmissions?.[0].rateRevision + if (rate.consolidatedStatus === 'WITHDRAWN' && latestRevision) { + acc.push(latestRevision) + } + return acc + }, [] as RateRevision[]) ?? [] + + const pageHeading = displayAsStandaloneRate + ? fetchRateData?.fetchRate.rate.draftRevision?.formData + .rateCertificationName + : contract?.draftRevision?.contractName + if (pageHeading) updateHeading({ customHeading: pageHeading }) + const [updateDraftContractRates] = useUpdateDraftContractRatesMutation() + const [submitRate] = useSubmitRateMutation() // Set up initial rate form values for Formik const initialRates: Rate[] = React.useMemo( @@ -394,8 +403,7 @@ const RateDetails = ({ )} + {withdrawnRateRevisions.length > + 0 && + withdrawnRateRevisions.map( + (rateRev) => ( + +

+ + WITHDRAWN + {' '} + { + rateRev + .formData + .rateCertificationName + } +

+
+ ) + )} )} diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.test.tsx index 7cea4819e4..edff3cf319 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.test.tsx @@ -11,6 +11,7 @@ import { mockContractPackageUnlockedWithUnlockedType, mockContractWithLinkedRateDraft, mockContractWithLinkedRateSubmitted, + mockWithdrawnRates, } from '@mc-review/mocks' import { renderWithProviders } from '../../../testHelpers/jestHelpers' import { RateDetailsSummarySection } from './RateDetailsSummarySection' @@ -224,9 +225,6 @@ describe('RateDetailsSummarySection', () => { // Is this the best way to check that the link is not present? expect(screen.queryByText('Edit')).not.toBeInTheDocument() - //expects loading button on component load - expect(screen.getByText('Loading')).toBeInTheDocument() - // expects download all button after loading has completed await waitFor(() => { expect( @@ -1253,6 +1251,7 @@ describe('RateDetailsSummarySection', () => { ).toBeInTheDocument() }) }) + it('displays deprecated fields on previous submissions viewed by state users', async () => { vi.spyOn( usePreviousSubmission, @@ -1301,6 +1300,7 @@ describe('RateDetailsSummarySection', () => { screen.findByText('Programs this rate certification covers') ).toBeTruthy() }) + it('does not display deprecated fields on unlocked submissions for state users', async () => { const draftContract = mockContractPackageDraft() if ( @@ -1333,4 +1333,53 @@ describe('RateDetailsSummarySection', () => { screen.queryByText('Programs this rate certification covers') ).toBeNull() }) + + it('displays withdrawn rates', async () => { + const contractWithWithdrawnRates = mockContractPackageSubmitted({ + withdrawnRates: mockWithdrawnRates(), + }) + + renderWithProviders( + , + { + apolloProvider: apolloProviderCMSUser, + } + ) + + expect( + screen.getByRole('heading', { + level: 2, + name: 'Rate details', + }) + ).toBeInTheDocument() + // Is this the best way to check that the link is not present? + expect(screen.queryByText('Edit')).not.toBeInTheDocument() + + // expects download all button after loading has completed + await waitFor(() => { + expect( + screen.getByRole('link', { + name: 'Download all rate documents', + }) + ).toBeInTheDocument() + }) + + // expect withdrawn rates to be on the screen + expect( + screen.getByRole('heading', { + level: 3, + name: /WITHDRAWN-RATE-1-NAME/, + }) + ).toBeInTheDocument() + expect( + screen.getByRole('heading', { + level: 3, + name: /WITHDRAWN-RATE-2-NAME/, + }) + ).toBeInTheDocument() + }) }) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.tsx index 7f87c14ff9..be57536f95 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/RateDetailsSummarySection.tsx @@ -41,6 +41,7 @@ import { useParams } from 'react-router-dom' import { LinkWithLogging } from '../../../components/TealiumLogging/Link' import classnames from 'classnames' import { hasCMSUserPermissions } from '@mc-review/helpers' +import { InfoTag } from '../../../components/InfoTag/InfoTag' export type RateDetailsSummarySectionProps = { contract: Contract | UnlockedContract @@ -69,7 +70,7 @@ type PackageNamesLookupType = { export function renderDownloadButton( zippedFilesURL: string | undefined | Error ) { - if (zippedFilesURL instanceof Error) { + if (zippedFilesURL instanceof Error || !zippedFilesURL) { return ( ) @@ -107,6 +108,15 @@ export const RateDetailsSummarySection = ({ ? rateRevisions : getVisibleLatestRateRevisions(contract, isEditing) + const withdrawnRateRevisions: RateRevision[] = + contract.withdrawnRates?.reduce((acc, rate) => { + const latestRevision = rate.packageSubmissions?.[0].rateRevision + if (rate.consolidatedStatus === 'WITHDRAWN' && latestRevision) { + acc.push(latestRevision) + } + return acc + }, [] as RateRevision[]) ?? [] + // Calculate last submitted data for document upload tables const lastSubmittedIndex = getIndexFromRevisionVersion( contract, @@ -227,8 +237,10 @@ export const RateDetailsSummarySection = ({ // get all the keys for the documents we want to zip async function fetchZipUrl() { const submittedRates = - getLastContractSubmission(contract)?.rateRevisions - if (submittedRates !== undefined) { + getLastContractSubmission(contract)?.rateRevisions ?? [] + + // skip if no rates + if (submittedRates.length > 0) { const keysFromDocs = submittedRates .flatMap((rateInfo) => rateInfo.formData.rateDocuments.concat( @@ -279,6 +291,12 @@ export const RateDetailsSummarySection = ({ isAdminUser && !isLinkedRate && !isPreviousSubmission, }) + const showDownloadAllButton = + isSubmittedOrCMSUser && + !isPreviousSubmission && + rateRevs && + rateRevs.length > 0 + const noRatesMessage = () => { if (isStateUser) { return isSubmitted @@ -299,9 +317,7 @@ export const RateDetailsSummarySection = ({ header="Rate details" editNavigateTo={editNavigateTo} > - {isSubmittedOrCMSUser && - !isPreviousSubmission && - renderDownloadButton(zippedFilesURL)} + {showDownloadAllButton && renderDownloadButton(zippedFilesURL)} {rateRevs && rateRevs.length > 0 ? rateRevs.map((rateRev) => { @@ -575,6 +591,21 @@ export const RateDetailsSummarySection = ({ : (isSubmitted || isStateUser) && ( )} + {withdrawnRateRevisions.length > 0 && + withdrawnRateRevisions.map((rateRev) => ( + +

+ WITHDRAWN{' '} + {rateRev.formData.rateCertificationName} +

+
+ ))} ) }