From 469a39f2e8cd3759be16db20b45a754818073a19 Mon Sep 17 00:00:00 2001 From: Jason Lin <98117700+JasonLin0991@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:14:22 -0400 Subject: [PATCH] MR-2364: Dealt with packages with shared rates in the current UI (#1945) * Update packagesWithSharedRateCerts to contractsWithSharedRateRevision. * Refactor packageName parameters to only accept what is used and move hanlding of no rate programs. * Add real packagesWithSharedRateCerts data from db to rateFormDataToDomainModel. Update conversion functions to return error types. * Refactor variable sub with pkg for less confusion. Replace setting value with formData id to pkg id. * Replace using getProgramsFromState with findStatePrograms and throwing errors in tests if programs are not found. * Fix imports * Update contractsWithSharedRateRevision * Update tests for packages with shared rate certs. * cypress re-run * cypress re-run * Fix some merging stuff. * cypress re-run --- .../migration.sql | 48 ++++ services/app-api/prisma/schema.prisma | 13 +- .../contractAndRates/updateInfoType.ts | 2 +- .../emailer/emails/newPackageCMSEmail.test.ts | 7 +- .../src/emailer/emails/newPackageCMSEmail.ts | 7 +- .../emails/newPackageStateEmail.test.ts | 14 +- .../emailer/emails/newPackageStateEmail.ts | 7 +- .../emails/resubmitPackageCMSEmail.test.ts | 7 +- .../emailer/emails/resubmitPackageCMSEmail.ts | 7 +- .../emails/resubmitPackageStateEmail.test.ts | 11 +- .../emails/resubmitPackageStateEmail.ts | 7 +- .../emails/unlockPackageCMSEmail.test.ts | 11 +- .../emailer/emails/unlockPackageCMSEmail.ts | 7 +- .../emails/unlockPackageStateEmail.test.ts | 11 +- .../emailer/emails/unlockPackageStateEmail.ts | 7 +- .../src/emailer/templateHelpers.test.ts | 2 +- services/app-api/src/handlers/reports.ts | 6 +- .../contractAndRates/insertContract.test.ts | 7 +- .../contractAndRates/insertRate.test.ts | 2 +- .../postgres/contractAndRates/insertRate.ts | 2 +- .../parseContractAndRates.test.ts | 1 + .../parseContractWithHistory.ts | 46 +++- .../contractAndRates/parseRateWithHistory.ts | 73 +++-- .../prismaDraftContractHelpers.ts | 36 ++- .../prismaDraftRatesHelpers.ts | 10 +- .../prismaSharedContractRateHelpers.ts | 75 ++++- .../updateDraftContractWithRates.ts | 36 ++- .../postgres/user/updateCmsUserProperties.ts | 2 +- .../contractAndRates/resolverHelper.test.ts | 26 +- .../contractAndRates/resolverHelpers.ts | 29 +- .../createHealthPlanPackage.ts | 3 +- .../fetchHealthPlanPackage.ts | 4 +- .../submitHealthPlanPackage.test.ts | 35 ++- .../unlockHealthPlanPackage.test.ts | 18 +- .../updateHealthPlanFormData.test.ts | 258 +++++++++++------- .../contractAndRates/contractHelpers.ts | 187 +++++++------ .../contractAndRates/rateHelpers.ts | 150 +++++----- .../app-api/src/testHelpers/gqlHelpers.ts | 11 +- services/app-api/src/testHelpers/index.ts | 2 +- .../src/testHelpers/launchDarklyHelpers.ts | 2 +- .../app-api/src/testHelpers/stateHelpers.ts | 11 +- .../healthPlanFormData.test.ts | 9 +- .../healthPlanFormData.ts | 31 ++- .../RateDetailsSummarySection.tsx | 7 +- .../UploadedDocumentsTable.test.tsx | 2 + .../src/pages/CMSDashboard/CMSDashboard.tsx | 7 +- .../pages/StateDashboard/StateDashboard.tsx | 7 +- .../PackagesWithSharedRates.tsx | 12 +- .../RateDetails/RateDetails.test.tsx | 3 + .../StateSubmission/StateSubmissionForm.tsx | 12 +- .../SubmissionRevisionSummary.tsx | 23 +- .../SubmissionSideNav/SubmissionSideNav.tsx | 14 +- .../SubmissionSummary/SubmissionSummary.tsx | 13 +- services/cypress/cypress.config.ts | 1 + 54 files changed, 918 insertions(+), 423 deletions(-) create mode 100644 services/app-api/prisma/migrations/20230922204522_modify_shared_rate_revision_relationship/migration.sql diff --git a/services/app-api/prisma/migrations/20230922204522_modify_shared_rate_revision_relationship/migration.sql b/services/app-api/prisma/migrations/20230922204522_modify_shared_rate_revision_relationship/migration.sql new file mode 100644 index 0000000000..0b5f3937b5 --- /dev/null +++ b/services/app-api/prisma/migrations/20230922204522_modify_shared_rate_revision_relationship/migration.sql @@ -0,0 +1,48 @@ +BEGIN; +/* + Warnings: + + - You are about to drop the `SharedRateCertifications` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_ContractRevisionTableToSharedRateCertifications` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_RateRevisionTableToSharedRateCertifications` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "_ContractRevisionTableToSharedRateCertifications" DROP CONSTRAINT "_ContractRevisionTableToSharedRateCertifications_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_ContractRevisionTableToSharedRateCertifications" DROP CONSTRAINT "_ContractRevisionTableToSharedRateCertifications_B_fkey"; + +-- DropForeignKey +ALTER TABLE "_RateRevisionTableToSharedRateCertifications" DROP CONSTRAINT "_RateRevisionTableToSharedRateCertifications_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_RateRevisionTableToSharedRateCertifications" DROP CONSTRAINT "_RateRevisionTableToSharedRateCertifications_B_fkey"; + +-- DropTable +DROP TABLE "SharedRateCertifications"; + +-- DropTable +DROP TABLE "_ContractRevisionTableToSharedRateCertifications"; + +-- DropTable +DROP TABLE "_RateRevisionTableToSharedRateCertifications"; + +-- CreateTable +CREATE TABLE "_SharedRateRevisions" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_SharedRateRevisions_AB_unique" ON "_SharedRateRevisions"("A", "B"); + +-- CreateIndex +CREATE INDEX "_SharedRateRevisions_B_index" ON "_SharedRateRevisions"("B"); + +-- AddForeignKey +ALTER TABLE "_SharedRateRevisions" ADD CONSTRAINT "_SharedRateRevisions_A_fkey" FOREIGN KEY ("A") REFERENCES "ContractTable"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_SharedRateRevisions" ADD CONSTRAINT "_SharedRateRevisions_B_fkey" FOREIGN KEY ("B") REFERENCES "RateRevisionTable"("id") ON DELETE CASCADE ON UPDATE CASCADE; +COMMIT; diff --git a/services/app-api/prisma/schema.prisma b/services/app-api/prisma/schema.prisma index fc1d4a71a7..dc51b687d3 100644 --- a/services/app-api/prisma/schema.prisma +++ b/services/app-api/prisma/schema.prisma @@ -45,6 +45,8 @@ model ContractTable { revisions ContractRevisionTable[] draftRateRevisions RateRevisionTable[] + + sharedRateRevisions RateRevisionTable[] @relation(name: "SharedRateRevisions") } model RateTable { @@ -66,7 +68,6 @@ model ContractRevisionTable { contract ContractTable @relation(fields: [contractID], references: [id]) rateRevisions RateRevisionsOnContractRevisionsTable[] - sharedRateRevisions SharedRateCertifications[] draftRates RateTable[] unlockInfoID String? @@ -138,7 +139,7 @@ model RateRevisionTable { certifyingActuaryContacts ActuaryContact[] @relation(name: "CertifyingActuaryOnRateRevision") addtlActuaryContacts ActuaryContact[] @relation(name: "AddtlActuaryOnRateRevision") actuaryCommunicationPreference ActuaryCommunication? - packagesWithSharedRateCerts SharedRateCertifications[] + contractsWithSharedRateRevision ContractTable[] @relation(name: "SharedRateRevisions") } model RateRevisionsOnContractRevisionsTable { @@ -166,14 +167,6 @@ model UpdateInfoTable { submittedRates RateRevisionTable[] @relation("submitRateInfo") } -model SharedRateCertifications { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - contract ContractRevisionTable[] - rateRevision RateRevisionTable[] -} - model ActuaryContact { id String @id @default(uuid()) createdAt DateTime @default(now()) diff --git a/services/app-api/src/domain-models/contractAndRates/updateInfoType.ts b/services/app-api/src/domain-models/contractAndRates/updateInfoType.ts index 3ad7c7ecaf..8b752211ff 100644 --- a/services/app-api/src/domain-models/contractAndRates/updateInfoType.ts +++ b/services/app-api/src/domain-models/contractAndRates/updateInfoType.ts @@ -10,6 +10,6 @@ const updateInfoSchema = z.object({ type UpdateInfoType = z.infer type PackageStatusType = z.infer -export type { PackageStatusType , UpdateInfoType } +export type { PackageStatusType, UpdateInfoType } export { updateInfoSchema } diff --git a/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts index c3327d8323..030d202799 100644 --- a/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts @@ -165,7 +165,12 @@ test('to addresses list does not include duplicate review email addresses', asyn test('subject line is correct', async () => { const sub = mockContractOnlyFormData() const statePrograms = mockMNState().programs - const name = packageName(sub, statePrograms) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + statePrograms + ) const template = await newPackageCMSEmail( sub, diff --git a/services/app-api/src/emailer/emails/newPackageCMSEmail.ts b/services/app-api/src/emailer/emails/newPackageCMSEmail.ts index 4b8426d2dd..9803b934e3 100644 --- a/services/app-api/src/emailer/emails/newPackageCMSEmail.ts +++ b/services/app-api/src/emailer/emails/newPackageCMSEmail.ts @@ -37,7 +37,12 @@ export const newPackageCMSEmail = async ( return packagePrograms } - const packageName = generatePackageName(pkg, packagePrograms) + const packageName = generatePackageName( + pkg.stateCode, + pkg.stateNumber, + pkg.programIDs, + packagePrograms + ) const packageURL = submissionSummaryURL(pkg.id, config.baseUrl) diff --git a/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts index 8df3d7cdfc..c16657803e 100644 --- a/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts @@ -114,7 +114,12 @@ test('to addresses list does not include duplicate state receiver emails on subm test('subject line is correct and clearly states submission is complete', async () => { const sub = mockContractOnlyFormData() const defaultStatePrograms = mockMNState().programs - const name = packageName(sub, defaultStatePrograms) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + defaultStatePrograms + ) const template = await newPackageStateEmail( sub, @@ -141,7 +146,12 @@ test('subject line is correct and clearly states submission is complete', async test('includes mcog, rate, and team email addresses', async () => { const sub = mockContractOnlyFormData() const defaultStatePrograms = mockMNState().programs - const name = packageName(sub, defaultStatePrograms) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + defaultStatePrograms + ) const template = await newPackageStateEmail( sub, diff --git a/services/app-api/src/emailer/emails/newPackageStateEmail.ts b/services/app-api/src/emailer/emails/newPackageStateEmail.ts index 4e5276c877..e0803c5f18 100644 --- a/services/app-api/src/emailer/emails/newPackageStateEmail.ts +++ b/services/app-api/src/emailer/emails/newPackageStateEmail.ts @@ -35,7 +35,12 @@ export const newPackageStateEmail = async ( return packagePrograms } - const packageName = generatePackageName(formData, packagePrograms) + const packageName = generatePackageName( + formData.stateCode, + formData.stateNumber, + formData.programIDs, + packagePrograms + ) const isContractAndRates = formData.submissionType === 'CONTRACT_AND_RATES' && diff --git a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts index 9c936b89f1..ebb4ff2dc2 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts @@ -24,7 +24,12 @@ describe('with rates', () => { const defaultStatePrograms = mockMNState().programs it('contains correct subject and clearly states submission edits are completed', async () => { - const name = packageName(submission, defaultStatePrograms) + const name = packageName( + submission.stateCode, + submission.stateNumber, + submission.programIDs, + defaultStatePrograms + ) const template = await resubmitPackageCMSEmail( submission, resubmitData, diff --git a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.ts b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.ts index 4483c6d9d3..dbd58f3703 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.ts @@ -37,7 +37,12 @@ export const resubmitPackageCMSEmail = async ( return packagePrograms } - const packageName = generatePackageName(pkg, packagePrograms) + const packageName = generatePackageName( + pkg.stateCode, + pkg.stateNumber, + pkg.programIDs, + packagePrograms + ) const isContractAndRates = pkg.submissionType === 'CONTRACT_AND_RATES' && diff --git a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts index 9fd6c72aa4..4a703584a9 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts @@ -4,11 +4,11 @@ import { mockMNState, } from '../../testHelpers/emailerHelpers' import { resubmitPackageStateEmail } from './index' -import type { LockedHealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { LockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { generateRateName, packageName, -} from 'app-web/src/common-code/healthPlanFormDataType' +} from '../../../../app-web/src/common-code/healthPlanFormDataType' const resubmitData = { updatedBy: 'bob@example.com', @@ -53,7 +53,12 @@ const defaultStatePrograms = mockMNState().programs const defaultSubmitters = ['test1@example.com', 'test2@example.com'] test('contains correct subject and clearly states successful resubmission', async () => { - const name = packageName(submission, defaultStatePrograms) + const name = packageName( + submission.stateCode, + submission.stateNumber, + submission.programIDs, + defaultStatePrograms + ) const template = await resubmitPackageStateEmail( submission, defaultSubmitters, diff --git a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.ts b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.ts index dbc3c03447..677067e4b5 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.ts @@ -36,7 +36,12 @@ export const resubmitPackageStateEmail = async ( return packagePrograms } - const packageName = generatePackageName(formData, packagePrograms) + const packageName = generatePackageName( + formData.stateCode, + formData.stateNumber, + formData.programIDs, + packagePrograms + ) const isContractAndRates = formData.submissionType === 'CONTRACT_AND_RATES' && diff --git a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts index be0117c69f..2360a1d67e 100644 --- a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts @@ -7,11 +7,11 @@ import { mockMSState, } from '../../testHelpers/emailerHelpers' import { unlockPackageCMSEmail } from './index' -import type { UnlockedHealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { UnlockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { generateRateName, packageName, -} from 'app-web/src/common-code/healthPlanFormDataType' +} from '../../../../app-web/src/common-code/healthPlanFormDataType' const unlockData = { updatedBy: 'leslie@example.com', @@ -59,7 +59,12 @@ const defaultStatePrograms = mockMNState().programs describe('unlockPackageCMSEmail', () => { test('subject line is correct and clearly states submission is unlocked', async () => { - const name = packageName(sub, defaultStatePrograms) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + defaultStatePrograms + ) const template = await unlockPackageCMSEmail( sub, unlockData, diff --git a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.ts b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.ts index 6e77a6e4f4..a15bbb862d 100644 --- a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.ts +++ b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.ts @@ -35,7 +35,12 @@ export const unlockPackageCMSEmail = async ( return packagePrograms } - const packageName = generatePackageName(pkg, packagePrograms) + const packageName = generatePackageName( + pkg.stateCode, + pkg.stateNumber, + pkg.programIDs, + packagePrograms + ) const isContractAndRates = pkg.submissionType === 'CONTRACT_AND_RATES' diff --git a/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts index 9719cb2b89..ab9bc17b99 100644 --- a/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts @@ -4,11 +4,11 @@ import { mockMNState, } from '../../testHelpers/emailerHelpers' import { unlockPackageStateEmail } from './index' -import type { UnlockedHealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { UnlockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { generateRateName, packageName, -} from 'app-web/src/common-code/healthPlanFormDataType' +} from '../../../../app-web/src/common-code/healthPlanFormDataType' const unlockData = { updatedBy: 'josh@example.com', @@ -56,7 +56,12 @@ const defaultStatePrograms = mockMNState().programs const defaultSubmitters = ['test1@example.com', 'test2@example.com'] test('subject line is correct and clearly states submission is unlocked', async () => { - const name = packageName(sub, defaultStatePrograms) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + defaultStatePrograms + ) const template = await unlockPackageStateEmail( sub, unlockData, diff --git a/services/app-api/src/emailer/emails/unlockPackageStateEmail.ts b/services/app-api/src/emailer/emails/unlockPackageStateEmail.ts index 36b7640f59..abbb6ca6ee 100644 --- a/services/app-api/src/emailer/emails/unlockPackageStateEmail.ts +++ b/services/app-api/src/emailer/emails/unlockPackageStateEmail.ts @@ -36,7 +36,12 @@ export const unlockPackageStateEmail = async ( return packagePrograms } - const packageName = generatePackageName(formData, packagePrograms) + const packageName = generatePackageName( + formData.stateCode, + formData.stateNumber, + formData.programIDs, + packagePrograms + ) const isContractAndRates = formData.submissionType === 'CONTRACT_AND_RATES' && diff --git a/services/app-api/src/emailer/templateHelpers.test.ts b/services/app-api/src/emailer/templateHelpers.test.ts index 6d2c29f545..a0d6590421 100644 --- a/services/app-api/src/emailer/templateHelpers.test.ts +++ b/services/app-api/src/emailer/templateHelpers.test.ts @@ -3,7 +3,7 @@ import { generateCMSReviewerEmails, handleAsCHIPSubmission, } from './templateHelpers' -import type { UnlockedHealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { UnlockedHealthPlanFormDataType } from '../../../app-web/src/common-code/healthPlanFormDataType' import { mockUnlockedContractAndRatesFormData, mockUnlockedContractOnlyFormData, diff --git a/services/app-api/src/handlers/reports.ts b/services/app-api/src/handlers/reports.ts index c1239e1da8..65e68c47d6 100644 --- a/services/app-api/src/handlers/reports.ts +++ b/services/app-api/src/handlers/reports.ts @@ -156,8 +156,12 @@ export const main: APIGatewayProxyHandler = async (event, context) => { ) } else { // add the package name to the revision + const { stateCode, stateNumber, programIDs } = + revision.formDataProto revision.packageName = packageName( - revision.formDataProto, + stateCode, + stateNumber, + programIDs, programList ) // add the rateInfo fields to the revision diff --git a/services/app-api/src/postgres/contractAndRates/insertContract.test.ts b/services/app-api/src/postgres/contractAndRates/insertContract.test.ts index 2bb8252293..89a17f127e 100644 --- a/services/app-api/src/postgres/contractAndRates/insertContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/insertContract.test.ts @@ -6,7 +6,7 @@ import { } from '../../testHelpers' import { insertDraftContract } from './insertContract' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' describe('insertContract', () => { afterEach(() => { @@ -82,9 +82,12 @@ describe('insertContract', () => { const client = await sharedTestPrismaClient() const draftContractData = createInsertContractData({ - stateCode: 'CANADA' as StateCodeType, + stateCode: 'MN' as StateCodeType, programIDs: [], }) + + draftContractData.stateCode = 'CANADA' + const draftContract = await insertDraftContract( client, draftContractData diff --git a/services/app-api/src/postgres/contractAndRates/insertRate.test.ts b/services/app-api/src/postgres/contractAndRates/insertRate.test.ts index 0d739a5f65..7fa5abe8fd 100644 --- a/services/app-api/src/postgres/contractAndRates/insertRate.test.ts +++ b/services/app-api/src/postgres/contractAndRates/insertRate.test.ts @@ -3,7 +3,7 @@ import { must, getStateRecord } from '../../testHelpers' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' import { insertDraftRate } from './insertRate' -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' describe('insertRate', () => { afterEach(() => { diff --git a/services/app-api/src/postgres/contractAndRates/insertRate.ts b/services/app-api/src/postgres/contractAndRates/insertRate.ts index 69a874cb5f..7e48aec617 100644 --- a/services/app-api/src/postgres/contractAndRates/insertRate.ts +++ b/services/app-api/src/postgres/contractAndRates/insertRate.ts @@ -1,4 +1,4 @@ -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { RateType } from '../../domain-models/contractAndRates' import { parseRateWithHistory } from './parseRateWithHistory' import { includeFullRate } from './prismaSubmittedRateHelpers' diff --git a/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts b/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts index 1a00067830..fef82ff8a8 100644 --- a/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts +++ b/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts @@ -185,6 +185,7 @@ describe('parseDomainData', () => { rateProgramIDs: [], rateCertificationName: null, actuaryCommunicationPreference: null, + contractsWithSharedRateRevision: [], }, }, ], diff --git a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts index ec73eb0c4a..efa15229ff 100644 --- a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts @@ -56,15 +56,26 @@ interface ContractRevisionSet { function contractSetsToDomainModel( revisions: ContractRevisionSet[] -): ContractRevisionWithRatesType[] { - const contractRevisions = revisions.map((entry) => ({ - ...contractRevisionToDomainModel(entry.contractRev), - rateRevisions: ratesRevisionsToDomainModel(entry.rateRevisions), +): ContractRevisionWithRatesType[] | Error { + const contractRevisions = [] - // override this contractRevisions's update infos with the one that caused this revision to be created. - submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), - unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), - })) + for (const revision of revisions) { + const rateRevisions = ratesRevisionsToDomainModel( + revision.rateRevisions + ) + + if (rateRevisions instanceof Error) { + return rateRevisions + } + + contractRevisions.push({ + ...contractRevisionToDomainModel(revision.contractRev), + rateRevisions, + // override this contractRevisions's update infos with the one that caused this revision to be created. + submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), + unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), + }) + } return contractRevisions } @@ -99,7 +110,8 @@ function contractWithHistoryToDomainModel( // between contract revision updates const allRevisionSets: ContractRevisionSet[] = [] const contractRevisions = contract.revisions - let draftRevision: ContractRevisionWithRatesType | undefined = undefined + let draftRevision: ContractRevisionWithRatesType | Error | undefined = + undefined for (const contractRev of contractRevisions) { // We set the draft revision aside, all ordered revisions are submitted if (!contractRev.submitInfo) { @@ -112,6 +124,12 @@ function contractWithHistoryToDomainModel( draftRevision = draftContractRevToDomainModel(contractRev) + if (draftRevision instanceof Error) { + return new Error( + `error converting draft contract revision with id ${contractRev.id} to domain model: ${draftRevision}` + ) + } + // skip the rest of the processing continue } @@ -173,7 +191,13 @@ function contractWithHistoryToDomainModel( } } - const revisions = contractSetsToDomainModel(allRevisionSets).reverse() + const revisions = contractSetsToDomainModel(allRevisionSets) + + if (revisions instanceof Error) { + return new Error( + `error converting contract with id ${contract.id} to domain models: ${draftRevision}` + ) + } return { id: contract.id, @@ -181,7 +205,7 @@ function contractWithHistoryToDomainModel( stateCode: contract.stateCode, stateNumber: contract.stateNumber, draftRevision, - revisions, + revisions: revisions.reverse(), } } diff --git a/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts index 0e5962535e..94671f50a3 100644 --- a/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts @@ -50,37 +50,65 @@ interface RateRevisionSet { function rateSetsToDomainModel( entries: RateRevisionSet[] -): RateRevisionWithContractsType[] { - const revisions = entries.map((entry) => ({ - ...rateRevisionToDomainModel(entry.rateRev), +): RateRevisionWithContractsType[] | Error { + const revisions: RateRevisionWithContractsType[] = [] - contractRevisions: contractRevisionsToDomainModels(entry.contractRevs), + for (const entry of entries) { + const domainRateRevision = rateRevisionToDomainModel(entry.rateRev) - // override this contractRevisions's update infos with the one that caused this revision to be created. - submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), - unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), - })) + if (domainRateRevision instanceof Error) { + return domainRateRevision + } + + revisions.push({ + ...domainRateRevision, + contractRevisions: contractRevisionsToDomainModels( + entry.contractRevs + ), + + // override this contractRevisions's update infos with the one that caused this revision to be created. + submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), + unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), + }) + } return revisions } function rateRevisionToDomainModel( revision: RateRevisionTableWithFormData -): RateRevisionType { +): RateRevisionType | Error { + const formData = rateFormDataToDomainModel(revision) + + if (formData instanceof Error) { + return formData + } + return { id: revision.id, createdAt: revision.createdAt, updatedAt: revision.updatedAt, submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), - - formData: rateFormDataToDomainModel(revision), + formData, } } function rateRevisionsToDomainModels( rateRevisions: RateRevisionTableWithFormData[] -): RateRevisionType[] { - return rateRevisions.map((crev) => rateRevisionToDomainModel(crev)) +): RateRevisionType[] | Error { + const domainRateRevisions: RateRevisionType[] = [] + + for (const rateRevision of rateRevisions) { + const domainRateRevision = rateRevisionToDomainModel(rateRevision) + + if (domainRateRevision instanceof Error) { + return domainRateRevision + } + + domainRateRevisions.push(domainRateRevision) + } + + return domainRateRevisions } // rateWithHistoryToDomainModel constructs a history for this particular contract including changes to all of its @@ -94,7 +122,8 @@ function rateWithHistoryToDomainModel( const allEntries: RateRevisionSet[] = [] const rateRevisions = rate.revisions - let draftRevision: RateRevisionWithContractsType | undefined = undefined + let draftRevision: RateRevisionWithContractsType | Error | undefined = + undefined for (const rateRev of rateRevisions) { // We have already set the draft revision aside, all ordered revisions here should be submitted if (!rateRev.submitInfo) { @@ -107,6 +136,12 @@ function rateWithHistoryToDomainModel( draftRevision = draftRateRevToDomainModel(rateRev) + if (draftRevision instanceof Error) { + return new Error( + `error converting draft rate revision with id ${rateRev.id} to domain model: ${draftRevision}` + ) + } + // skip the rest of the processing continue } @@ -164,7 +199,13 @@ function rateWithHistoryToDomainModel( } } - const revisions = rateSetsToDomainModel(allEntries).reverse() + const revisions = rateSetsToDomainModel(allEntries) + + if (revisions instanceof Error) { + return new Error( + `error converting rate with id ${rate.id} to domain models: ${draftRevision}` + ) + } return { id: rate.id, @@ -172,7 +213,7 @@ function rateWithHistoryToDomainModel( stateCode: rate.stateCode, stateNumber: rate.stateNumber, draftRevision, - revisions, + revisions: revisions.reverse(), } } export { diff --git a/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts index a7384d986a..8a4236dfc6 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts @@ -36,6 +36,16 @@ const includeDraftRates = { }, submitInfo: includeUpdateInfo, unlockInfo: includeUpdateInfo, + contractsWithSharedRateRevision: { + include: { + revisions: { + take: 1, + orderBy: { + createdAt: 'desc', + }, + }, + }, + }, }, take: 1, orderBy: { @@ -50,22 +60,40 @@ type DraftRatesTable = Prisma.RateTableGetPayload<{ function draftRatesToDomainModel( draftRates: DraftRatesTable[] -): RateRevisionType[] { - return draftRates.map((dr) => rateRevisionToDomainModel(dr.revisions[0])) +): RateRevisionType[] | Error { + const domainRates: RateRevisionType[] = [] + + for (const rate of draftRates) { + const domainRate = rateRevisionToDomainModel(rate.revisions[0]) + + if (domainRate instanceof Error) { + return domainRate + } + + domainRates.push(domainRate) + } + + return domainRates } // ------------------- function draftContractRevToDomainModel( revision: ContractRevisionTableWithRates -): ContractRevisionWithRatesType { +): ContractRevisionWithRatesType | Error { + const rateRevisions = draftRatesToDomainModel(revision.draftRates) + + if (rateRevisions instanceof Error) { + return rateRevisions + } + return { id: revision.id, createdAt: revision.createdAt, updatedAt: revision.updatedAt, unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), formData: contractFormDataToDomainModel(revision), - rateRevisions: draftRatesToDomainModel(revision.draftRates), + rateRevisions, } } diff --git a/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts index 3afeb2942e..26d5e30468 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts @@ -41,12 +41,18 @@ function draftContractsToDomainModel( function draftRateRevToDomainModel( revision: RateRevisionTableWithContracts -): RateRevisionWithContractsType { +): RateRevisionWithContractsType | Error { + const formData = rateFormDataToDomainModel(revision) + + if (formData instanceof Error) { + return formData + } + return { id: revision.id, createdAt: revision.createdAt, updatedAt: revision.updatedAt, - formData: rateFormDataToDomainModel(revision), + formData, contractRevisions: draftContractsToDomainModel(revision.draftContracts), } } diff --git a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts index 5049c9a521..f6e084ccf5 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts @@ -1,5 +1,6 @@ import type { Prisma } from '@prisma/client' -import type { DocumentCategoryType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { DocumentCategoryType } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import type { ProgramType } from '../../domain-models' import type { ContractFormDataType, RateFormDataType, @@ -7,6 +8,8 @@ import type { PackageStatusType, UpdateInfoType, } from '../../domain-models/contractAndRates' +import { findStatePrograms } from '../state' +import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' const subincludeUpdateInfo = { updatedBy: true, @@ -85,6 +88,16 @@ const includeRateFormData = { position: 'asc', }, }, + contractsWithSharedRateRevision: { + include: { + revisions: { + take: 1, + orderBy: { + createdAt: 'desc', + }, + }, + }, + }, } satisfies Prisma.RateRevisionTableInclude type RateRevisionTableWithFormData = Prisma.RateRevisionTableGetPayload<{ @@ -93,7 +106,34 @@ type RateRevisionTableWithFormData = Prisma.RateRevisionTableGetPayload<{ function rateFormDataToDomainModel( rateRevision: RateRevisionTableWithFormData -): RateFormDataType { +): RateFormDataType | Error { + const packagesWithSharedRateCerts = [] + let statePrograms: ProgramType[] | Error | undefined = undefined + + for (const contract of rateRevision.contractsWithSharedRateRevision) { + const contractPrograms = contract.revisions[0].programIDs + + if (!statePrograms) { + statePrograms = findStatePrograms(contract.stateCode) + } + + if (statePrograms instanceof Error) { + return new Error( + `Cannot find ${contract.stateCode} programs for packagesWithSharedRateCerts with rate revision ${rateRevision.rateID} and contract ${contract.id}` + ) + } + + packagesWithSharedRateCerts.push({ + packageId: contract.id, + packageName: packageName( + contract.stateCode, + contract.stateNumber, + contractPrograms, + statePrograms + ), + }) + } + return { id: rateRevision.id, rateID: rateRevision.rateID, @@ -146,33 +186,48 @@ function rateFormDataToDomainModel( : [], actuaryCommunicationPreference: rateRevision.actuaryCommunicationPreference ?? undefined, - packagesWithSharedRateCerts: [], // intentionally not handling packagesWithSharedRates yet - this is MR-3568 + packagesWithSharedRateCerts, } } function rateRevisionToDomainModel( revision: RateRevisionTableWithFormData -): RateRevisionType { +): RateRevisionType | Error { + const formData = rateFormDataToDomainModel(revision) + + if (formData instanceof Error) { + return formData + } + return { id: revision.id, createdAt: revision.createdAt, updatedAt: revision.updatedAt, unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), - - formData: rateFormDataToDomainModel(revision), + formData, } } function ratesRevisionsToDomainModel( rateRevisions: RateRevisionTableWithFormData[] -): RateRevisionType[] { - const domainRevisions = rateRevisions.map((rrev) => - rateRevisionToDomainModel(rrev) - ) +): RateRevisionType[] | Error { + const domainRevisions: RateRevisionType[] = [] + + for (const revision of rateRevisions) { + const domainRevision = rateRevisionToDomainModel(revision) + + if (domainRevision instanceof Error) { + return domainRevision + } + + domainRevisions.push(domainRevision) + } + domainRevisions.sort( (a, b) => a.createdAt.getTime() - b.createdAt.getTime() ) + return domainRevisions } diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts index a21a31b7a2..076275a4ed 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts @@ -7,7 +7,7 @@ import type { RateRevisionType, ContractType, } from '../../domain-models/contractAndRates' -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { includeDraftRates } from './prismaDraftContractHelpers' import { rateRevisionToDomainModel } from './prismaSharedContractRateHelpers' import type { RateFormEditable } from './updateDraftRate' @@ -144,9 +144,23 @@ async function updateDraftContractWithRates( } const stateCode = currentRev.contract.stateCode as StateCodeType - const ratesFromDB = currentRev.draftRates.map((rate) => - rateRevisionToDomainModel(rate.revisions[0]) - ) + const ratesFromDB: RateRevisionType[] = [] + + // Convert all rates from DB to domain model + for (const rate of currentRev.draftRates) { + const domainRateRevision = rateRevisionToDomainModel( + rate.revisions[0] + ) + + if (domainRateRevision instanceof Error) { + return new Error( + `Error updating contract with rates: ${domainRateRevision.message}` + ) + } + + ratesFromDB.push(domainRateRevision) + } + const updateRates = rateFormDatas && sortRatesForUpdate(ratesFromDB, rateFormDatas) @@ -179,6 +193,13 @@ async function updateDraftContractWithRates( }) : undefined + const contractsWithSharedRates = + rateRevision.packagesWithSharedRateCerts?.map( + (pkg) => ({ + id: pkg.packageId, + }) + ) ?? [] + // If rate does not exist, we need to create a new rate. if (!currentRate) { const { latestStateRateCertNumber } = @@ -257,6 +278,9 @@ async function updateDraftContractWithRates( }, actuaryCommunicationPreference: rateRevision.actuaryCommunicationPreference, + contractsWithSharedRateRevision: { + connect: contractsWithSharedRates, + }, }, }, draftContractRevisions: { @@ -363,6 +387,10 @@ async function updateDraftContractWithRates( nullify( rateRevision.actuaryCommunicationPreference ), + contractsWithSharedRateRevision: + { + set: contractsWithSharedRates, + }, }, }, } diff --git a/services/app-api/src/postgres/user/updateCmsUserProperties.ts b/services/app-api/src/postgres/user/updateCmsUserProperties.ts index e683b95efb..43ca0044e3 100644 --- a/services/app-api/src/postgres/user/updateCmsUserProperties.ts +++ b/services/app-api/src/postgres/user/updateCmsUserProperties.ts @@ -1,6 +1,6 @@ import type { StoreError } from '../storeError' import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { Division, PrismaClient } from '@prisma/client' import { AuditAction } from '@prisma/client' import type { CMSUserType } from '../../domain-models' diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts index 04a6209469..be101bbbbe 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts @@ -202,7 +202,20 @@ describe('convertHealthPlanPackageRatesToDomain', () => { }, ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', - packagesWithSharedRateCerts: [], + packagesWithSharedRateCerts: [ + { + packageName: 'testABC1', + packageId: 'test-abc-1', + }, + { + packageName: undefined, + packageId: 'test-abc-2', + }, + { + packageName: 'testABC3', + packageId: undefined, + }, + ], }, ], stateContacts: [], @@ -281,7 +294,16 @@ describe('convertHealthPlanPackageRatesToDomain', () => { }, ], actuaryCommunicationPreference: 'OACT_TO_ACTUARY', - packagesWithSharedRateCerts: [], + packagesWithSharedRateCerts: [ + { + packageId: 'test-abc-1', + packageName: 'testABC1', + }, + { + packageId: 'test-abc-2', + packageName: '', + }, + ], addtlActuaryContacts: [ { actuarialFirm: 'DELOITTE', diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts index cd6061c42b..51eceee1bf 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts @@ -99,6 +99,30 @@ const convertHealthPlanPackageRatesToDomain = async ( hppRateFormData.supportingDocuments ) + // We only care about the contract ID. PackageName is not in our DB, instead when converting DB data to domain + // data we are generating the package name. + const packagesWithSharedRateCerts = + hppRateFormData.packagesWithSharedRateCerts?.reduce( + ( + accumulator: RateFormDataType['packagesWithSharedRateCerts'] = [], + currentValue + ) => { + // Skipping over any shared rate contract that has no ID. + if (currentValue.packageId) { + const shared: { + packageName: string + packageId: string + } = { + packageName: currentValue.packageName ?? '', + packageId: currentValue.packageId, + } + accumulator.push(shared) + } + return accumulator + }, + [] + ) + const rate: RateFormDataType = { id: hppRateFormData.id, rateType: hppRateFormData.rateType, @@ -122,10 +146,7 @@ const convertHealthPlanPackageRatesToDomain = async ( // toProtobuffer already does this, so we can directly set the value from the rate data. actuaryCommunicationPreference: hppRateFormData.actuaryCommunicationPreference, - // This field is set to empty array because we still need to figure out shared rates. This is MR-3568 - packagesWithSharedRateCerts: [], - // packagesWithSharedRateCerts: hppRateFormData.packagesWithSharedRateCerts && - // hppRateFormData.packagesWithSharedRateCerts.filter(rate => (rate.packageId && rate.packageName)) as RateFormDataType['packagesWithSharedRateCerts'] + packagesWithSharedRateCerts, } const parsedDomainData = rateFormDataSchema.safeParse(rate) diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts index 0949a10361..3dd8d38195 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts @@ -92,8 +92,7 @@ export function createHealthPlanPackageResolver( } // Now we do the conversions - const pkg = - convertContractWithRatesToUnlockedHPP(contractResult) + const pkg = convertContractWithRatesToUnlockedHPP(contractResult) if (pkg instanceof Error) { const errMessage = `Error converting draft contract. Message: ${pkg.message}` diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index f4d3c07100..4416a31b9b 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -5,7 +5,7 @@ import { isStateUser, isAdminUser, packageStatus, - convertContractWithRatesToUnlockedHPP, + convertContractWithRatesToUnlockedHPP, } from '../../domain-models' import { isHelpdeskUser } from '../../domain-models/user' import type { QueryResolvers, State } from '../../gen/gqlServer' @@ -66,7 +66,7 @@ export function fetchHealthPlanPackageResolver( } const convertedPkg = - convertContractWithRatesToUnlockedHPP(contractWithHistory) + convertContractWithRatesToUnlockedHPP(contractWithHistory) if (convertedPkg instanceof Error) { const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts index 9f91915872..1b0a54c6d5 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts @@ -560,7 +560,12 @@ describe.each(flagValueTestParameters)( } const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) const stateAnalystsEmails = getTestStateAnalystsEmails( sub.stateCode ) @@ -673,7 +678,12 @@ describe.each(flagValueTestParameters)( const programs = [defaultFloridaProgram()] const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) const rateName = generateRateName( sub, sub.rateInfos[0], @@ -724,7 +734,12 @@ describe.each(flagValueTestParameters)( } const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) expect(mockEmailer.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ @@ -779,7 +794,12 @@ describe.each(flagValueTestParameters)( } const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) // email subject line is correct for CMS email and contains correct email body text expect(mockEmailer.sendEmail).toHaveBeenCalledWith( @@ -847,7 +867,12 @@ describe.each(flagValueTestParameters)( } const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) // email subject line is correct for CMS email and contains correct email body text expect(mockEmailer.sendEmail).toHaveBeenCalledWith( diff --git a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts index 0359449102..bb212ba918 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts @@ -20,11 +20,11 @@ import { import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { mockStoreThatErrors } from '../../testHelpers/storeHelpers' import { testEmailConfig, testEmailer } from '../../testHelpers/emailerHelpers' -import { base64ToDomain } from 'app-web/src/common-code/proto/healthPlanFormDataProto' +import { base64ToDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' import { generateRateName, packageName, -} from 'app-web/src/common-code/healthPlanFormDataType' +} from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { HealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' import { getTestStateAnalystsEmails, @@ -748,7 +748,12 @@ describe.each(flagValueTestParameters)( const programs = [defaultFloridaProgram()] const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) const rateName = generateRateName( sub, sub.rateInfos[0], @@ -834,7 +839,12 @@ describe.each(flagValueTestParameters)( const programs = [defaultFloridaProgram()] const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName(sub, programs) + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) const rateName = generateRateName( sub, sub.rateInfos[0], diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts index 20a68ee448..c065a25320 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts @@ -1,35 +1,31 @@ -import { - constructTestPostgresServer, - createAndSubmitTestHealthPlanPackage, - createTestHealthPlanPackage, -} from '../../testHelpers/gqlHelpers' +import { v4 as uuidv4 } from 'uuid' +import { findStatePrograms, NewPostgresStore } from '../../postgres' +import * as add_sha from '../../handlers/add_sha' +import { submitContract } from '../../postgres/contractAndRates/submitContract' import UPDATE_HEALTH_PLAN_FORM_DATA from '../../../../app-graphql/src/mutations/updateHealthPlanFormData.graphql' import { domainToBase64 } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' +import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { basicLockedHealthPlanFormData, basicHealthPlanFormData, } from '../../../../app-web/src/common-code/healthPlanFormDataMocks' -import { v4 as uuidv4 } from 'uuid' +import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { mockStoreThatErrors, sharedTestPrismaClient, } from '../../testHelpers/storeHelpers' -import { NewPostgresStore } from '../../postgres' +import { + constructTestPostgresServer, + createAndSubmitTestHealthPlanPackage, + createTestHealthPlanPackage, +} from '../../testHelpers/gqlHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' +import { testLDService } from '../../testHelpers/launchDarklyHelpers' +import { must } from '../../testHelpers' import type { FeatureFlagLDConstant, FlagValue, } from '../../../../app-web/src/common-code/featureFlags' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' -import { getProgramsFromState, must } from '../../testHelpers' -import { submitContract } from '../../postgres/contractAndRates/submitContract' -import type { - HealthPlanFormDataType, - RateInfoType, - StateCodeType, -} from '../../../../app-web/src/common-code/healthPlanFormDataType' -import * as add_sha from '../../handlers/add_sha' const flagValueTestParameters: { flagName: FeatureFlagLDConstant @@ -69,47 +65,44 @@ describe.each(flagValueTestParameters)( const createdDraft = await createTestHealthPlanPackage(server) // update that draft. - const formData: HealthPlanFormDataType = Object.assign( - latestFormData(createdDraft), - { - programIDs: [], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_ONLY', - riskBasedContract: true, - submissionDescription: 'Updated submission', - stateContacts: [], - documents: [], - contractType: 'BASE', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [], - contractDateStart: new Date(Date.UTC(2025, 5, 1)), - contractDateEnd: new Date(Date.UTC(2026, 5, 1)), - managedCareEntities: ['MCO'], - federalAuthorities: [], - contractAmendmentInfo: { - modifiedProvisions: { - inLieuServicesAndSettings: true, - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: true, - modifiedMedicaidBeneficiaries: true, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: true, - modifiedWitholdAgreements: true, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: false, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: undefined, - modifiedLengthOfContract: undefined, - modifiedNonRiskPaymentArrangements: undefined, - }, + const formData = Object.assign(latestFormData(createdDraft), { + programIDs: [], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_ONLY', + riskBasedContract: true, + submissionDescription: 'Updated submission', + stateContacts: [], + documents: [], + contractType: 'BASE', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [], + contractDateStart: new Date(Date.UTC(2025, 5, 1)), + contractDateEnd: new Date(Date.UTC(2026, 5, 1)), + managedCareEntities: ['MCO'], + federalAuthorities: [], + contractAmendmentInfo: { + modifiedProvisions: { + inLieuServicesAndSettings: true, + modifiedBenefitsProvided: true, + modifiedGeoAreaServed: true, + modifiedMedicaidBeneficiaries: true, + modifiedRiskSharingStrategy: true, + modifiedIncentiveArrangements: true, + modifiedWitholdAgreements: true, + modifiedStateDirectedPayments: true, + modifiedPassThroughPayments: false, + modifiedPaymentsForMentalDiseaseInstitutions: false, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, }, - rateInfos: [], - } - ) + }, + rateInfos: [], + }) // convert to base64 proto const updatedB64 = domainToBase64(formData) @@ -139,16 +132,65 @@ describe.each(flagValueTestParameters)( }) it('creates, updates, and deletes rates in the contract', async () => { + const stateUser = { + id: uuidv4(), + givenName: 'Aang', + familyName: 'Avatar', + email: 'aang@example.com', + role: 'STATE_USER' as const, + stateCode: 'MN', + } const server = await constructTestPostgresServer({ ldService: mockLDService, + context: { + user: stateUser, + }, }) - const createdDraft = await createTestHealthPlanPackage(server) - const ratePrograms = getProgramsFromState( - createdDraft.stateCode as StateCodeType + + const stateCode = 'MN' + const createdDraft = await createTestHealthPlanPackage( + server, + stateCode + ) + const statePrograms = must( + findStatePrograms(createdDraft.stateCode) + ) + + // Create 2 valid contracts to attached to packagesWithSharedRateCerts + const createdDraftTwo = await createTestHealthPlanPackage( + server, + stateCode + ) + const createdDraftThree = await createTestHealthPlanPackage( + server, + stateCode ) + const createdDraftTwoFormData = latestFormData(createdDraftTwo) + const createdDraftThreeFormData = latestFormData(createdDraftThree) + + const packageWithSharedRate1 = { + packageId: createdDraftTwo.id, + packageName: packageName( + createdDraftTwo.stateCode, + createdDraftTwoFormData.stateNumber, + createdDraftTwoFormData.programIDs, + statePrograms + ), + } as const + + const packageWithSharedRate2 = { + packageId: createdDraftThree.id, + packageName: packageName( + createdDraftThree.stateCode, + createdDraftThreeFormData.stateNumber, + createdDraftThreeFormData.programIDs, + statePrograms + ), + } as const + // Create 2 rate data for insertion - const rate1: RateInfoType = { + const rate1 = { rateType: 'NEW' as const, rateDateStart: new Date(Date.UTC(2025, 5, 1)), rateDateEnd: new Date(Date.UTC(2026, 4, 30)), @@ -166,7 +208,7 @@ describe.each(flagValueTestParameters)( rateCertificationName: undefined, supportingDocuments: [], //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [ratePrograms.reverse()[0].id], + rateProgramIDs: [statePrograms.reverse()[0].id], actuaryContacts: [ { name: 'test name', @@ -176,7 +218,10 @@ describe.each(flagValueTestParameters)( actuarialFirmOther: '', }, ], - packagesWithSharedRateCerts: [], + packagesWithSharedRateCerts: [ + packageWithSharedRate1, + packageWithSharedRate2, + ], } const rate2 = { @@ -197,7 +242,7 @@ describe.each(flagValueTestParameters)( rateCertificationName: undefined, supportingDocuments: [], //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [ratePrograms.reverse()[0].id], + rateProgramIDs: [statePrograms.reverse()[0].id], actuaryContacts: [ { name: 'test name', @@ -211,12 +256,9 @@ describe.each(flagValueTestParameters)( } // update that draft form data. - const formData: HealthPlanFormDataType = Object.assign( - latestFormData(createdDraft), - { - rateInfos: [rate1, rate2], - } - ) + const formData = Object.assign(latestFormData(createdDraft), { + rateInfos: [rate1, rate2], + }) // convert to base64 proto const updatedB64 = domainToBase64(formData) @@ -249,6 +291,22 @@ describe.each(flagValueTestParameters)( ...rate1, id: expect.any(String), rateCertificationName: expect.any(String), + packagesWithSharedRateCerts: expect.arrayContaining( + [ + expect.objectContaining({ + packageId: + packageWithSharedRate1.packageId, + packageName: + packageWithSharedRate1.packageName, + }), + expect.objectContaining({ + packageId: + packageWithSharedRate2.packageId, + packageName: + packageWithSharedRate2.packageName, + }), + ] + ), }), expect.objectContaining({ ...rate2, @@ -270,19 +328,19 @@ describe.each(flagValueTestParameters)( rateCertificationName: undefined, supportingDocuments: [], //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [ratePrograms.reverse()[0].id], + rateProgramIDs: [statePrograms.reverse()[0].id], actuaryContacts: [], packagesWithSharedRateCerts: [], } // Update first rate and remove second from contract and add a new rate. - const formData2: HealthPlanFormDataType = Object.assign( + const formData2 = Object.assign( latestFormData(updatedHealthPlanPackage), { rateInfos: [ - // updating the actuary on the first rate { ...updatedFormData.rateInfos[0], + // updating the actuary on the first rate actuaryContacts: [ { name: 'New actuary', @@ -292,6 +350,8 @@ describe.each(flagValueTestParameters)( actuarialFirmOther: '', }, ], + // remove second package with shared rate, by only passing in the first + packagesWithSharedRateCerts: [], }, { ...rate3, @@ -330,6 +390,7 @@ describe.each(flagValueTestParameters)( ...formData2.rateInfos[0], id: expect.any(String), rateCertificationName: expect.any(String), + packagesWithSharedRateCerts: [], }), expect.objectContaining({ ...formData2.rateInfos[1], @@ -349,33 +410,30 @@ describe.each(flagValueTestParameters)( const createdDraft = await createTestHealthPlanPackage(server) // update that draft. - const formData: HealthPlanFormDataType = Object.assign( - latestFormData(createdDraft), - { - programIDs: [], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_ONLY', - riskBasedContract: true, - submissionDescription: 'Updated submission', - stateContacts: [ - { - name: 'statecontact', - titleRole: 'thestatestofcontacts', - email: 'statemcstate@examepl.com', - }, - ], - documents: [ - { - name: 'supportingDocument11.pdf', - s3URL: 'fakeS3URL', - documentCategories: ['CONTRACT_RELATED' as const], - sha256: 'needs-to-be-there', - }, - ], - adsfdas: 'sdfsdf', - rateInfos: [], - } - ) + const formData = Object.assign(latestFormData(createdDraft), { + programIDs: [], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_ONLY', + riskBasedContract: true, + submissionDescription: 'Updated submission', + stateContacts: [ + { + name: 'statecontact', + titleRole: 'thestatestofcontacts', + email: 'statemcstate@examepl.com', + }, + ], + documents: [ + { + name: 'supportingDocument11.pdf', + s3URL: 'fakeS3URL', + documentCategories: ['CONTRACT_RELATED' as const], + sha256: 'needs-to-be-there', + }, + ], + adsfdas: 'sdfsdf', + rateInfos: [], + }) // convert to base64 proto const updatedB64 = domainToBase64(formData) diff --git a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts index 1c1ac0dcc5..c12b45ae14 100644 --- a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts +++ b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts @@ -6,7 +6,8 @@ import type { } from '../../postgres/contractAndRates/prismaSubmittedContractHelpers' import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { ContractFormDataType } from '../../domain-models/contractAndRates' -import { getProgramsFromState } from '../stateHelpers' +import { findStatePrograms } from '../../postgres' +import { must } from '../errorHelpers' const createInsertContractData = ({ stateCode = 'MN', @@ -14,15 +15,15 @@ const createInsertContractData = ({ }: { stateCode?: StateCodeType } & Partial): InsertContractArgsType => { + const statePrograms = must(findStatePrograms(stateCode)) + return { stateCode: stateCode, submissionType: formData?.submissionType ?? 'CONTRACT_AND_RATES', submissionDescription: formData?.submissionDescription ?? 'Contract 1.0', contractType: formData?.contractType ?? 'BASE', - programIDs: formData?.programIDs ?? [ - getProgramsFromState(stateCode ?? 'MN')[0].id, - ], + programIDs: formData?.programIDs ?? [statePrograms[0].id], populationCovered: formData?.populationCovered ?? 'MEDICAID', riskBasedContract: formData?.riskBasedContract ?? false, } @@ -70,98 +71,102 @@ const createContractData = ( const createContractRevision = ( revision?: Partial, stateCode: StateCodeType = 'MN' -): ContractRevisionTableWithRates => ({ - id: uuidv4(), - createdAt: new Date(), - updatedAt: new Date(), - submitInfo: { +): ContractRevisionTableWithRates => { + const statePrograms = must(findStatePrograms(stateCode)) + + return { id: uuidv4(), + createdAt: new Date(), updatedAt: new Date(), - updatedByID: 'someone', - updatedReason: 'submit', - updatedBy: { - id: 'someone', - createdAt: new Date(), - updatedAt: new Date(), - givenName: 'Bob', - familyName: 'Law', - email: 'boblaw@example.com', - role: 'STATE_USER', - divisionAssignment: null, - stateCode: stateCode, - }, - }, - unlockInfo: null, - contractID: 'contractID', - submitInfoID: null, - unlockInfoID: null, - programIDs: [getProgramsFromState(stateCode)[0].id], - populationCovered: 'MEDICAID' as const, - submissionType: 'CONTRACT_ONLY' as const, - riskBasedContract: false, - submissionDescription: 'Test', - stateContacts: [], - supportingDocuments: [ - { - id: uuidv4(), - position: 0, - contractRevisionID: 'contractRevisionID', - createdAt: new Date(), - updatedAt: new Date(), - name: 'contract supporting doc', - s3URL: 'fakeS3URL', - sha256: '2342fwlkdmwvw', - }, - { - id: uuidv4(), - position: 1, - contractRevisionID: 'contractRevisionID', - createdAt: new Date(), - updatedAt: new Date(), - name: 'contract supporting doc 2', - s3URL: 'fakeS3URL', - sha256: '45662342fwlkdmwvw', - }, - ], - contractType: 'BASE', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [ - { + submitInfo: { id: uuidv4(), - position: 0, - contractRevisionID: 'contractRevisionID', - createdAt: new Date(), updatedAt: new Date(), - name: 'contract doc', - s3URL: 'fakeS3URL', - sha256: '8984234fwlkdmwvw', + updatedByID: 'someone', + updatedReason: 'submit', + updatedBy: { + id: 'someone', + createdAt: new Date(), + updatedAt: new Date(), + givenName: 'Bob', + familyName: 'Law', + email: 'boblaw@example.com', + role: 'STATE_USER', + divisionAssignment: null, + stateCode: stateCode, + }, }, - ], - contractDateStart: new Date(Date.UTC(2025, 5, 1)), - contractDateEnd: new Date(Date.UTC(2026, 4, 30)), - managedCareEntities: ['MCO'], - federalAuthorities: ['STATE_PLAN' as const], - modifiedBenefitsProvided: false, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: false, - modifiedRiskSharingStrategy: false, - modifiedIncentiveArrangements: false, - modifiedWitholdAgreements: false, - modifiedStateDirectedPayments: false, - modifiedPassThroughPayments: false, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: true, - modifiedLengthOfContract: true, - modifiedNonRiskPaymentArrangements: null, - inLieuServicesAndSettings: null, - rateRevisions: [], - draftRates: [], - ...revision, -}) + unlockInfo: null, + contractID: 'contractID', + submitInfoID: null, + unlockInfoID: null, + programIDs: [statePrograms[0].id], + populationCovered: 'MEDICAID' as const, + submissionType: 'CONTRACT_ONLY' as const, + riskBasedContract: false, + submissionDescription: 'Test', + stateContacts: [], + supportingDocuments: [ + { + id: uuidv4(), + position: 0, + contractRevisionID: 'contractRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'contract supporting doc', + s3URL: 'fakeS3URL', + sha256: '2342fwlkdmwvw', + }, + { + id: uuidv4(), + position: 1, + contractRevisionID: 'contractRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'contract supporting doc 2', + s3URL: 'fakeS3URL', + sha256: '45662342fwlkdmwvw', + }, + ], + contractType: 'BASE', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [ + { + id: uuidv4(), + position: 0, + contractRevisionID: 'contractRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'contract doc', + s3URL: 'fakeS3URL', + sha256: '8984234fwlkdmwvw', + }, + ], + contractDateStart: new Date(Date.UTC(2025, 5, 1)), + contractDateEnd: new Date(Date.UTC(2026, 4, 30)), + managedCareEntities: ['MCO'], + federalAuthorities: ['STATE_PLAN' as const], + modifiedBenefitsProvided: false, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: false, + modifiedRiskSharingStrategy: false, + modifiedIncentiveArrangements: false, + modifiedWitholdAgreements: false, + modifiedStateDirectedPayments: false, + modifiedPassThroughPayments: false, + modifiedPaymentsForMentalDiseaseInstitutions: false, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: true, + modifiedLengthOfContract: true, + modifiedNonRiskPaymentArrangements: null, + inLieuServicesAndSettings: null, + rateRevisions: [], + draftRates: [], + ...revision, + } +} export { createInsertContractData, diff --git a/services/app-api/src/testHelpers/contractAndRates/rateHelpers.ts b/services/app-api/src/testHelpers/contractAndRates/rateHelpers.ts index 4cbb296dc7..a9baf0faba 100644 --- a/services/app-api/src/testHelpers/contractAndRates/rateHelpers.ts +++ b/services/app-api/src/testHelpers/contractAndRates/rateHelpers.ts @@ -4,8 +4,9 @@ import type { RateTableFullPayload, RateRevisionTableWithContracts, } from '../../postgres/contractAndRates/prismaSubmittedRateHelpers' -import { getProgramsFromState } from '../stateHelpers' -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { findStatePrograms } from '../../postgres' +import { must } from '../errorHelpers' const createInsertRateData = ( rateArgs?: Partial @@ -58,81 +59,86 @@ const createRateData = ( const createRateRevision = ( revision?: Partial, stateCode: StateCodeType = 'MN' -): RateRevisionTableWithContracts => ({ - id: uuidv4(), - createdAt: new Date(), - updatedAt: new Date(), - submitInfo: { +): RateRevisionTableWithContracts => { + const statePrograms = must(findStatePrograms(stateCode)) + + return { id: uuidv4(), + createdAt: new Date(), updatedAt: new Date(), - updatedByID: 'someone', - updatedReason: 'submit', - updatedBy: { - id: 'someone', - createdAt: new Date(), - updatedAt: new Date(), - givenName: 'Bob', - familyName: 'Law', - email: 'boblaw@example.com', - role: 'STATE_USER', - divisionAssignment: null, - stateCode: stateCode, - }, - }, - unlockInfo: null, - submitInfoID: null, - unlockInfoID: null, - rateType: 'NEW', - rateID: 'rateID', - rateCertificationName: 'testState-123', - rateProgramIDs: [getProgramsFromState(stateCode)[0].id], - rateCapitationType: 'RATE_CELL', - rateDateStart: new Date(), - rateDateEnd: new Date(), - rateDateCertified: new Date(), - amendmentEffectiveDateEnd: new Date(), - amendmentEffectiveDateStart: new Date(), - actuaryCommunicationPreference: 'OACT_TO_ACTUARY', - certifyingActuaryContacts: [], - addtlActuaryContacts: [], - supportingDocuments: [ - { - id: uuidv4(), - position: 0, - rateRevisionID: 'rateRevisionID', - createdAt: new Date(), - updatedAt: new Date(), - name: 'rate supporting doc', - s3URL: 'fakeS3URL', - sha256: '2342fwlkdmwvw', - }, - { - id: uuidv4(), - position: 1, - rateRevisionID: 'rateRevisionID', - createdAt: new Date(), - updatedAt: new Date(), - name: 'rate supporting doc 2', - s3URL: 'fakeS3URL', - sha256: '45662342fwlkdmwvw', - }, - ], - rateDocuments: [ - { + submitInfo: { id: uuidv4(), - position: 0, - rateRevisionID: 'rateRevisionID', - createdAt: new Date(), updatedAt: new Date(), - name: 'contract doc', - s3URL: 'fakeS3URL', - sha256: '8984234fwlkdmwvw', + updatedByID: 'someone', + updatedReason: 'submit', + updatedBy: { + id: 'someone', + createdAt: new Date(), + updatedAt: new Date(), + givenName: 'Bob', + familyName: 'Law', + email: 'boblaw@example.com', + role: 'STATE_USER', + divisionAssignment: null, + stateCode: stateCode, + }, }, - ], - contractRevisions: [], - draftContracts: [], - ...revision, -}) + unlockInfo: null, + submitInfoID: null, + unlockInfoID: null, + rateType: 'NEW', + rateID: 'rateID', + rateCertificationName: 'testState-123', + rateProgramIDs: [statePrograms[0].id], + rateCapitationType: 'RATE_CELL', + rateDateStart: new Date(), + rateDateEnd: new Date(), + rateDateCertified: new Date(), + amendmentEffectiveDateEnd: new Date(), + amendmentEffectiveDateStart: new Date(), + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + certifyingActuaryContacts: [], + addtlActuaryContacts: [], + supportingDocuments: [ + { + id: uuidv4(), + position: 0, + rateRevisionID: 'rateRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'rate supporting doc', + s3URL: 'fakeS3URL', + sha256: '2342fwlkdmwvw', + }, + { + id: uuidv4(), + position: 1, + rateRevisionID: 'rateRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'rate supporting doc 2', + s3URL: 'fakeS3URL', + sha256: '45662342fwlkdmwvw', + }, + ], + rateDocuments: [ + { + id: uuidv4(), + position: 0, + rateRevisionID: 'rateRevisionID', + createdAt: new Date(), + updatedAt: new Date(), + name: 'contract doc', + s3URL: 'fakeS3URL', + sha256: '8984234fwlkdmwvw', + }, + ], + contractRevisions: [], + draftContracts: [], + contractsWithSharedRateRevision: [], + ...revision, + } +} export { createInsertRateData, diff --git a/services/app-api/src/testHelpers/gqlHelpers.ts b/services/app-api/src/testHelpers/gqlHelpers.ts index 1318d82188..70fef932e5 100644 --- a/services/app-api/src/testHelpers/gqlHelpers.ts +++ b/services/app-api/src/testHelpers/gqlHelpers.ts @@ -12,7 +12,7 @@ import type { HealthPlanFormDataType, UnlockedHealthPlanFormDataType, StateCodeType, -} from 'app-web/src/common-code/healthPlanFormDataType' +} from '../../../app-web/src/common-code/healthPlanFormDataType' import type { CreateQuestionInput, InsertQuestionResponseArgs, @@ -33,14 +33,15 @@ import { NewPostgresStore } from '../postgres' import { configureResolvers } from '../resolvers' import { latestFormData } from './healthPlanPackageHelpers' import { sharedTestPrismaClient } from './storeHelpers' -import { domainToBase64 } from 'app-web/src/common-code/proto/healthPlanFormDataProto' +import { domainToBase64 } from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' import type { EmailParameterStore } from '../parameterStore' import { newLocalEmailParameterStore } from '../parameterStore' import { testLDService } from './launchDarklyHelpers' import type { LDService } from '../launchDarkly/launchDarkly' import { insertUserToLocalAurora } from '../authn' import { testStateUser } from './userHelpers' -import { getProgramsFromState } from './stateHelpers' +import { findStatePrograms } from '../postgres' +import { must } from './errorHelpers' // Since our programs are checked into source code, we have a program we // use as our default @@ -120,7 +121,7 @@ const createTestHealthPlanPackage = async ( stateCode?: StateCodeType ): Promise => { const programs = stateCode - ? getProgramsFromState(stateCode) + ? [must(findStatePrograms(stateCode))[0]] : [defaultFloridaProgram()] const programIDs = programs.map((program) => program.id) @@ -185,7 +186,7 @@ const createAndUpdateTestHealthPlanPackage = async ( const draft = latestFormData(pkg) const ratePrograms = stateCode - ? getProgramsFromState(stateCode) + ? [must(findStatePrograms(stateCode))[0]] : [defaultFloridaRateProgram()] ;(draft.submissionType = 'CONTRACT_AND_RATES' as const), diff --git a/services/app-api/src/testHelpers/index.ts b/services/app-api/src/testHelpers/index.ts index 1ed448dc40..4a6e418ab6 100644 --- a/services/app-api/src/testHelpers/index.ts +++ b/services/app-api/src/testHelpers/index.ts @@ -13,4 +13,4 @@ export { createDraftContractData, } from './contractAndRates/contractHelpers' -export { getProgramsFromState, getStateRecord } from './stateHelpers' +export { getStateRecord } from './stateHelpers' diff --git a/services/app-api/src/testHelpers/launchDarklyHelpers.ts b/services/app-api/src/testHelpers/launchDarklyHelpers.ts index 8332be0d91..15d149e6e8 100644 --- a/services/app-api/src/testHelpers/launchDarklyHelpers.ts +++ b/services/app-api/src/testHelpers/launchDarklyHelpers.ts @@ -2,7 +2,7 @@ import type { LDService } from '../launchDarkly/launchDarkly' import type { FeatureFlagLDConstant, FeatureFlagSettings, -} from 'app-web/src/common-code/featureFlags' +} from '../../../app-web/src/common-code/featureFlags' import { defaultFeatureFlags } from '../launchDarkly/launchDarkly' diff --git a/services/app-api/src/testHelpers/stateHelpers.ts b/services/app-api/src/testHelpers/stateHelpers.ts index cec1ba1d30..3ca67befc7 100644 --- a/services/app-api/src/testHelpers/stateHelpers.ts +++ b/services/app-api/src/testHelpers/stateHelpers.ts @@ -1,15 +1,6 @@ -import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' -import type { ProgramType } from '../domain-models' -import statePrograms from 'app-web/src/common-code/data/statePrograms.json' import type { PrismaClient, State } from '@prisma/client' import { must } from './errorHelpers' -function getProgramsFromState(stateCode: StateCodeType): ProgramType[] { - const state = statePrograms.states.find((st) => st.code === stateCode) - - return state?.programs || [] -} - async function getStateRecord( client: PrismaClient, stateCode: string @@ -29,4 +20,4 @@ async function getStateRecord( return state } -export { getProgramsFromState, getStateRecord } +export { getStateRecord } diff --git a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts index 4fff671752..8e619b4d3a 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts @@ -541,7 +541,14 @@ describe('submission type assertions', () => { const sub = basicHealthPlanFormData() sub.programIDs = programIDs - expect(packageName(sub, programs)).toBe(expectedName) + expect( + packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + ).toBe(expectedName) }) const mockContractAndRateSub = mockContractAndRatesDraft() diff --git a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts index 65b6d15dcd..2a87abe4dd 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts @@ -240,17 +240,13 @@ function programNames( } function packageName( - pkg: HealthPlanFormDataType, - statePrograms: ProgramArgType[], - programIDs?: string[] + stateCode: string, + stateNumber: number, + programIDs: string[], + statePrograms: ProgramArgType[] ): string { - const padNumber = pkg.stateNumber.toString().padStart(4, '0') - const pNames = - // This ternary is needed because programIDs passed in could be undefined or an empty string, in that case - // we want to default to using programIDs from submission - programIDs && programIDs.length > 0 - ? programNames(statePrograms, programIDs) - : programNames(statePrograms, pkg.programIDs) + const padNumber = stateNumber.toString().padStart(4, '0') + const pNames = programNames(statePrograms, programIDs) const formattedProgramNames = pNames .sort(naturalSort) .map((n) => @@ -260,7 +256,7 @@ function packageName( .toUpperCase() ) .join('-') - return `MCR-${pkg.stateCode.toUpperCase()}-${padNumber}-${formattedProgramNames}` + return `MCR-${stateCode.toUpperCase()}-${padNumber}-${formattedProgramNames}` } const generateRateName = ( @@ -274,10 +270,19 @@ const generateRateName = ( rateDateCertified, rateDateEnd, rateDateStart, - rateProgramIDs, } = rateInfo - let rateName = `${packageName(pkg, statePrograms, rateProgramIDs)}-RATE` + // Default to package programs if rate programs do not exist + const rateProgramIDs = rateInfo.rateProgramIDs?.length + ? rateInfo.rateProgramIDs + : pkg.programIDs + + let rateName = `${packageName( + pkg.stateCode, + pkg.stateNumber, + rateProgramIDs, + statePrograms + )}-RATE` if (rateType === 'AMENDMENT' && rateAmendmentInfo?.effectiveDateStart) { rateName = rateName.concat( diff --git a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx index c8c65cd875..78f338b6a0 100644 --- a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx @@ -137,7 +137,12 @@ export const RateDetailsSummarySection = ({ const [_, currentSubmissionData] = currentRevisionPackageOrError acc[pkg.id] = { - packageName: packageName(currentSubmissionData, statePrograms), + packageName: packageName( + currentSubmissionData.stateCode, + currentSubmissionData.stateNumber, + currentSubmissionData.programIDs, + statePrograms + ), status: pkg.status, } diff --git a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx index c18e1c79f2..d9f47ac99d 100644 --- a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx @@ -418,11 +418,13 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', documentCategories: ['CONTRACT_RELATED' as const], + sha256: 'fakeSha1', }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', documentCategories: ['RATES_RELATED' as const], + sha256: 'fakeSha2', }, ] diff --git a/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx b/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx index 0ec5b09903..3096ef6403 100644 --- a/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx +++ b/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx @@ -184,7 +184,12 @@ const SubmissionsDashboard = (): React.ReactElement => { submissionRows.push({ id: sub.id, - name: packageName(packageDataToDisplay, programs), + name: packageName( + packageDataToDisplay.stateCode, + packageDataToDisplay.stateNumber, + packageDataToDisplay.programIDs, + programs + ), programs: programs.filter((program) => packageDataToDisplay.programIDs.includes(program.id) ), diff --git a/services/app-web/src/pages/StateDashboard/StateDashboard.tsx b/services/app-web/src/pages/StateDashboard/StateDashboard.tsx index 86879ad5d9..9c3844051f 100644 --- a/services/app-web/src/pages/StateDashboard/StateDashboard.tsx +++ b/services/app-web/src/pages/StateDashboard/StateDashboard.tsx @@ -73,7 +73,12 @@ export const StateDashboard = (): React.ReactElement => { submissionRows.push({ id: sub.id, - name: packageName(currentSubmissionData, programs), + name: packageName( + currentSubmissionData.stateCode, + currentSubmissionData.stateNumber, + currentSubmissionData.programIDs, + programs + ), programs: programs.filter((program) => currentSubmissionData.programIDs.includes(program.id) ), diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx index 8ef6c5c6e1..7abfee8a0f 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx @@ -57,12 +57,12 @@ export const PackagesWithSharedRates = ({ > = [] data?.indexHealthPlanPackages.edges .map((edge) => edge.node) - .forEach((sub) => { + .forEach((pkg) => { const currentRevisionPackageOrError = - getCurrentRevisionFromHealthPlanPackage(sub) + getCurrentRevisionFromHealthPlanPackage(pkg) if (currentRevisionPackageOrError instanceof Error) { recordJSException( - `indexHealthPlanPackagesQuery: Error decoding proto. ID: ${sub.id}` + `indexHealthPlanPackagesQuery: Error decoding proto. ID: ${pkg.id}` ) return null // TODO make an error state for PackageSelect, right now we just remove from page if this request fails } @@ -87,10 +87,12 @@ export const PackagesWithSharedRates = ({ packagesWithUpdatedAt.push({ updatedAt: currentSubmissionData.updatedAt, label: `${packageName( - currentSubmissionData, + currentSubmissionData.stateCode, + currentSubmissionData.stateNumber, + currentSubmissionData.programIDs, statePrograms )}${submittedAt}`, - value: currentSubmissionData.id, + value: pkg.id, }) } }) 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 f39e6895b0..6d9b955403 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx @@ -1328,16 +1328,19 @@ describe('RateDetails', () => { s3URL: 's3://bucketname/one-one/one-one.png', name: 'one one', documentCategories: ['CONTRACT_RELATED'], + sha256: 'fakeSha1', }, { s3URL: 's3://bucketname/one-two/one-two.png', name: 'one two', documentCategories: ['CONTRACT_RELATED'], + sha256: 'fakeSha2', }, { s3URL: 's3://bucketname/one-three/one-three.png', name: 'one three', documentCategories: ['CONTRACT_RELATED'], + sha256: 'fakeSha2', }, ] renderWithProviders( diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx index d8014551c1..38d4589f5f 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx @@ -115,9 +115,13 @@ export type HealthPlanFormPageProps = { input: UnlockedHealthPlanFormDataType ) => Promise } + +type RouteParams = { + id: string +} + export const StateSubmissionForm = (): React.ReactElement => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const { id } = useParams<{ id: string }>() + const { id } = useParams() // IF not id throw new error if (!id) { throw new Error( @@ -221,7 +225,9 @@ export const StateSubmissionForm = (): React.ReactElement => { } const computedSubmissionName = packageName( - formDataFromLatestRevision, + formDataFromLatestRevision.stateCode, + formDataFromLatestRevision.stateNumber, + formDataFromLatestRevision.programIDs, statePrograms ) if (pkgNameForHeading !== computedSubmissionName) { diff --git a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx index 3ce975b62b..0d996f5156 100644 --- a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx +++ b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx @@ -21,12 +21,14 @@ import { useFetchHealthPlanPackageWrapper } from '../../gqlHelpers' import { ApolloError } from '@apollo/client' import { handleApolloError } from '../../gqlHelpers/apolloErrors' +type RouteParams = { + id: string + revisionVersion: string +} + export const SubmissionRevisionSummary = (): React.ReactElement => { // Page level state - const { id, revisionVersion } = useParams<{ - id: string - revisionVersion: string - }>() + const { id, revisionVersion } = useParams() if (!id) { throw new Error( 'PROGRAMMING ERROR: id param not set in state submission form.' @@ -79,7 +81,12 @@ export const SubmissionRevisionSummary = (): React.ReactElement => { const packageData = revisionsLookup[revision.id].formData const statePrograms = pkg.state.programs - const name = packageName(packageData, statePrograms) + const name = packageName( + packageData.stateCode, + packageData.stateNumber, + packageData.programIDs, + statePrograms + ) if (pkgName !== name) { setPkgName(name) } @@ -100,7 +107,7 @@ export const SubmissionRevisionSummary = (): React.ReactElement => { { {isContractActionAndRateCertification && ( )} diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx index 51d6be48d8..50d67642ae 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx @@ -43,9 +43,12 @@ export type SideNavOutletContextType = { user: User } +type RouteParams = { + id: string +} + export const SubmissionSideNav = () => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const { id } = useParams<{ id: string }>() + const { id } = useParams() if (!id) { throw new Error( 'PROGRAMMING ERROR: id param not set in state submission form.' @@ -135,7 +138,12 @@ export const SubmissionSideNav = () => { } const currentRevision = edge.node const packageData = revisionsLookup[currentRevision.id].formData - const pkgName = packageName(packageData, pkg.state.programs) + const pkgName = packageName( + packageData.stateCode, + packageData.stateNumber, + packageData.programIDs, + pkg.state.programs + ) const documentDates = makeDocumentDateTable(revisionsLookup) const outletContext: SideNavOutletContextType = { pkg, diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx index b0a9965f43..afdaf632d3 100644 --- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx +++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx @@ -76,7 +76,12 @@ export const SubmissionSummary = (): React.ReactElement => { const statePrograms = pkg.state.programs // set the page heading - const name = packageName(packageData, statePrograms) + const name = packageName( + packageData.stateCode, + packageData.stateNumber, + packageData.programIDs, + statePrograms + ) if (pkgName !== name) { setPkgName(name) } @@ -157,7 +162,7 @@ export const SubmissionSummary = (): React.ReactElement => { { documentDateLookupTable={documentDates} submission={packageData} isCMSUser={isCMSUser} - submissionName={packageName(packageData, statePrograms)} + submissionName={name} onDocumentError={handleDocumentDownloadError} /> @@ -181,7 +186,7 @@ export const SubmissionSummary = (): React.ReactElement => {