diff --git a/services/app-api/src/domain-models/contractAndRates/index.ts b/services/app-api/src/domain-models/contractAndRates/index.ts index bca12e667a..91ade24b14 100644 --- a/services/app-api/src/domain-models/contractAndRates/index.ts +++ b/services/app-api/src/domain-models/contractAndRates/index.ts @@ -1,7 +1,5 @@ export { rateSchema, draftRateSchema } from './rateTypes' -export type { RateType } from './rateTypes' - export { contractSchema, draftContractSchema } from './contractTypes' export { contractFormDataSchema, rateFormDataSchema } from './formDataTypes' @@ -16,13 +14,13 @@ export { export { convertContractWithRatesRevtoHPPRev, convertContractWithRatesToUnlockedHPP, - convertContractWithRatesToFormData + convertContractWithRatesToFormData, } from './convertContractWithRatesToHPP' - export type { ContractType } from './contractTypes' +export type { RateType } from './rateTypes' -export type { PackageStatusType , UpdateInfoType } from './updateInfoType' +export type { PackageStatusType, UpdateInfoType } from './updateInfoType' export type { ContractFormDataType, RateFormDataType } from './formDataTypes' diff --git a/services/app-api/src/domain-models/index.ts b/services/app-api/src/domain-models/index.ts index f4c299e78b..73c011347b 100644 --- a/services/app-api/src/domain-models/index.ts +++ b/services/app-api/src/domain-models/index.ts @@ -25,11 +25,23 @@ export { packageSubmitters, } from './healthPlanPackage' -export { +export { convertContractWithRatesRevtoHPPRev, convertContractWithRatesToUnlockedHPP, } from './contractAndRates' +export type { + ContractType, + ContractRevisionType, + ContractRevisionWithRatesType, + ContractFormDataType, + RateType, + RateRevisionType, + RateRevisionWithContractsType, + RateFormDataType, + PackageStatusType, +} from './contractAndRates' + export type { HealthPlanRevisionType, HealthPlanPackageType, diff --git a/services/app-api/src/postgres/contractAndRates/index.ts b/services/app-api/src/postgres/contractAndRates/index.ts index 46c9a05405..3a66211255 100644 --- a/services/app-api/src/postgres/contractAndRates/index.ts +++ b/services/app-api/src/postgres/contractAndRates/index.ts @@ -1,5 +1,9 @@ export type { InsertContractArgsType } from './insertContract' export type { UpdateContractArgsType } from './updateDraftContract' +export type { SubmitContractArgsType } from './submitContract' +export type { SubmitRateArgsType } from './submitRate' +export { submitContract } from './submitContract' +export { submitRate } from './submitRate' export { insertDraftContract } from './insertContract' export { findContractWithHistory } from './findContractWithHistory' export { updateDraftContract } from './updateDraftContract' diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.ts b/services/app-api/src/postgres/contractAndRates/submitContract.ts index 0680119800..832f1a89b5 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.ts @@ -6,7 +6,7 @@ import type { UpdateInfoType } from '../../domain-models' import { includeFirstSubmittedRateRev } from './prismaSubmittedRateHelpers' type SubmitContractArgsType = { - contractID: string + contractID: string // revision ID submittedByUserID: UpdateInfoType['updatedBy'] submitReason: UpdateInfoType['updatedReason'] } @@ -156,3 +156,4 @@ async function submitContract( } export { submitContract } +export type { SubmitContractArgsType } diff --git a/services/app-api/src/postgres/contractAndRates/submitRate.ts b/services/app-api/src/postgres/contractAndRates/submitRate.ts index 45ac23241c..36f22a7406 100644 --- a/services/app-api/src/postgres/contractAndRates/submitRate.ts +++ b/services/app-api/src/postgres/contractAndRates/submitRate.ts @@ -47,19 +47,6 @@ async function submitRate( (c) => c.revisions[0] ) - const everyRelatedContractIsSubmitted = relatedContractRevs.every( - (rev) => rev !== undefined - ) - - if (!everyRelatedContractIsSubmitted) { - const message = - 'Attempted to submit a rate related to a contract that has not been submitted' - - console.error(message) - - return new Error(message) - } - const updated = await tx.rateRevisionTable.update({ where: { id: currentRev.id, @@ -158,3 +145,4 @@ async function submitRate( } export { submitRate } +export type { SubmitRateArgsType } diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index 72a37787f6..307d785f2d 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -7,7 +7,7 @@ import type { ContractFormDataType } from '../../domain-models/contractAndRates' type ContractFormEditable = Partial type UpdateContractArgsType = { - contractID: string + contractID: string //revision ID formData: ContractFormEditable rateIDs: string[] } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 2fb7891ee6..4aab8c6c66 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -20,6 +20,7 @@ import type { QuestionResponseType, InsertQuestionResponseArgs, StateType, + RateType, } from '../domain-models' import { findPrograms, findStatePrograms } from '../postgres' import type { StoreError } from './storeError' @@ -52,10 +53,14 @@ import { insertDraftContract, findContractWithHistory, updateDraftContract, + submitRate, + submitContract, } from './contractAndRates' import type { InsertContractArgsType, UpdateContractArgsType, + SubmitContractArgsType, + SubmitRateArgsType, } from './contractAndRates' type Store = { @@ -145,6 +150,12 @@ type Store = { updateDraftContract: ( args: UpdateContractArgsType ) => Promise + + submitContract: ( + args: SubmitContractArgsType + ) => Promise + + submitRate: (args: SubmitRateArgsType) => Promise } function NewPostgresStore(client: PrismaClient): Store { @@ -206,6 +217,8 @@ function NewPostgresStore(client: PrismaClient): Store { findContractWithHistory: (args) => findContractWithHistory(client, args), updateDraftContract: (args) => updateDraftContract(client, args), + submitContract: (args) => submitContract(client, args), + submitRate: (args) => submitRate(client, args), } } diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts index 9e8641afe3..ec12589936 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts @@ -24,7 +24,10 @@ import { } from '../../testHelpers/parameterStoreHelpers' import * as awsSESHelpers from '../../testHelpers/awsSESHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import type{ FeatureFlagLDConstant, FlagValue } from 'app-web/src/common-code/featureFlags' +import type { + FeatureFlagLDConstant, + FlagValue, +} from 'app-web/src/common-code/featureFlags' const flagValueTestParameters: { flagName: FeatureFlagLDConstant @@ -46,255 +49,235 @@ const flagValueTestParameters: { describe.each(flagValueTestParameters)( `Tests $testName`, ({ flagName, flagValue }) => { - const cmsUser = testCMSUser() - it.only('returns a StateSubmission if complete', async () => { - const server = await constructTestPostgresServer() - - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - {} - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const cmsUser = testCMSUser() + it('returns a StateSubmission if complete', async () => { + const server = await constructTestPostgresServer() + + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + {} + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id + + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) - - expect(submitResult.errors).toBeUndefined() - const createdID = submitResult?.data?.submitHealthPlanPackage.pkg.id - - // test result - const pkg = await fetchTestHealthPlanPackageById(server, createdID) - - const resultDraft = latestFormData(pkg) - - // The submission fields should still be set - expect(resultDraft.id).toEqual(createdID) - expect(resultDraft.submissionType).toBe('CONTRACT_AND_RATES') - expect(resultDraft.programIDs).toEqual([defaultFloridaProgram().id]) - // check that the stateNumber is being returned the same - expect(resultDraft.stateNumber).toEqual(draft.stateNumber) - expect(resultDraft.submissionDescription).toBe('An updated submission') - expect(resultDraft.documents).toEqual(draft.documents) - - // Contract details fields should still be set - expect(resultDraft.contractType).toEqual(draft.contractType) - expect(resultDraft.contractExecutionStatus).toEqual( - draft.contractExecutionStatus - ) - expect(resultDraft.contractDateStart).toEqual(draft.contractDateStart) - expect(resultDraft.contractDateEnd).toEqual(draft.contractDateEnd) - expect(resultDraft.managedCareEntities).toEqual( - draft.managedCareEntities - ) - expect(resultDraft.contractDocuments).toEqual(draft.contractDocuments) - - expect(resultDraft.federalAuthorities).toEqual(draft.federalAuthorities) - - if (resultDraft.status == 'DRAFT') { - throw new Error('Not a locked submission') - } - - // submittedAt should be set to today's date - const today = new Date() - const expectedDate = today.toISOString().split('T')[0] - expect(pkg.initiallySubmittedAt).toEqual(expectedDate) - - // UpdatedAt should be after the former updatedAt - const resultUpdated = new Date(resultDraft.updatedAt) - const createdUpdated = new Date(draft.updatedAt) - expect( - resultUpdated.getTime() - createdUpdated.getTime() - ).toBeGreaterThan(0) - }, 20000) + }) - it('returns an error if there are no contract documents attached', async () => { - const server = await constructTestPostgresServer() + expect(submitResult.errors).toBeUndefined() + const createdID = submitResult?.data?.submitHealthPlanPackage.pkg.id + + // test result + const pkg = await fetchTestHealthPlanPackageById(server, createdID) + + const resultDraft = latestFormData(pkg) + + // The submission fields should still be set + expect(resultDraft.id).toEqual(createdID) + expect(resultDraft.submissionType).toBe('CONTRACT_AND_RATES') + expect(resultDraft.programIDs).toEqual([defaultFloridaProgram().id]) + // check that the stateNumber is being returned the same + expect(resultDraft.stateNumber).toEqual(draft.stateNumber) + expect(resultDraft.submissionDescription).toBe( + 'An updated submission' + ) + expect(resultDraft.documents).toEqual(draft.documents) + + // Contract details fields should still be set + expect(resultDraft.contractType).toEqual(draft.contractType) + expect(resultDraft.contractExecutionStatus).toEqual( + draft.contractExecutionStatus + ) + expect(resultDraft.contractDateStart).toEqual( + draft.contractDateStart + ) + expect(resultDraft.contractDateEnd).toEqual(draft.contractDateEnd) + expect(resultDraft.managedCareEntities).toEqual( + draft.managedCareEntities + ) + expect(resultDraft.contractDocuments).toEqual( + draft.contractDocuments + ) + + expect(resultDraft.federalAuthorities).toEqual( + draft.federalAuthorities + ) + + if (resultDraft.status == 'DRAFT') { + throw new Error('Not a locked submission') + } - const draft = await createAndUpdateTestHealthPlanPackage(server, { - documents: [], - contractDocuments: [], - }) - const draftID = draft.id + // submittedAt should be set to today's date + const today = new Date() + const expectedDate = today.toISOString().split('T')[0] + expect(pkg.initiallySubmittedAt).toEqual(expectedDate) + + // UpdatedAt should be after the former updatedAt + const resultUpdated = new Date(resultDraft.updatedAt) + const createdUpdated = new Date(draft.updatedAt) + expect( + resultUpdated.getTime() - createdUpdated.getTime() + ).toBeGreaterThan(0) + }, 20000) + + it('returns an error if there are no contract documents attached', async () => { + const server = await constructTestPostgresServer() + + const draft = await createAndUpdateTestHealthPlanPackage(server, { + documents: [], + contractDocuments: [], + }) + const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) - - expect(submitResult.errors).toBeDefined() - - expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData must have valid documents' - ) - }) - - it('returns an error if the package is already SUBMITTED', async () => { - const server = await constructTestPostgresServer() + }) - const draft = await createAndSubmitTestHealthPlanPackage(server) - const draftID = draft.id + expect(submitResult.errors).toBeDefined() - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, + expect(submitResult.errors?.[0].extensions?.code).toBe( + 'BAD_USER_INPUT' + ) + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData must have valid documents' + ) }) - expect(submitResult.errors).toBeDefined() + it('returns an error if the package is already SUBMITTED', async () => { + const server = await constructTestPostgresServer() + + const draft = await createAndSubmitTestHealthPlanPackage(server) + const draftID = draft.id - expect(submitResult.errors?.[0].extensions).toEqual( - expect.objectContaining({ - code: 'INTERNAL_SERVER_ERROR', - cause: 'INVALID_PACKAGE_STATUS', - exception: { - locations: undefined, - message: - 'Attempted to submit an already submitted package.', - path: undefined, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, }) - ) - expect(submitResult.errors?.[0].message).toBe( - 'Attempted to submit an already submitted package.' - ) - }) + expect(submitResult.errors).toBeDefined() - it('returns an error if there are no contract details fields', async () => { - const server = await constructTestPostgresServer() - - const draft = await createAndUpdateTestHealthPlanPackage(server, { - contractType: undefined, - contractExecutionStatus: undefined, - managedCareEntities: [], - federalAuthorities: [], - }) + expect(submitResult.errors?.[0].extensions).toEqual( + expect.objectContaining({ + code: 'INTERNAL_SERVER_ERROR', + cause: 'INVALID_PACKAGE_STATUS', + exception: { + locations: undefined, + message: + 'Attempted to submit an already submitted package.', + path: undefined, + }, + }) + ) - const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, + expect(submitResult.errors?.[0].message).toBe( + 'Attempted to submit an already submitted package.' + ) }) - expect(submitResult.errors).toBeDefined() - - expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) - }) + it('returns an error if there are no contract details fields', async () => { + const server = await constructTestPostgresServer() - it('returns an error if there are missing rate details fields for submission type', async () => { - const server = await constructTestPostgresServer() + const draft = await createAndUpdateTestHealthPlanPackage(server, { + contractType: undefined, + contractExecutionStatus: undefined, + managedCareEntities: [], + federalAuthorities: [], + }) - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', - rateInfos: [ - { - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 'fakeS3URL', - documentCategories: ['RATES' as const], - }, - ], - supportingDocuments: [], - rateProgramIDs: ['3b8d8fa1-1fa6-4504-9c5b-ef522877fe1e'], - actuaryContacts: [], - actuaryCommunicationPreference: 'OACT_TO_ACTUARY' as const, - packagesWithSharedRateCerts: [], + const draftID = draft.id + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - ], - }) + }) - const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, - }) + expect(submitResult.errors).toBeDefined() - expect(submitResult.errors).toBeDefined() + expect(submitResult.errors?.[0].extensions?.code).toBe( + 'BAD_USER_INPUT' + ) + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }) - expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required rate fields' - ) - }) + it('returns an error if there are missing rate details fields for submission type', async () => { + const server = await constructTestPostgresServer() - it('does not remove any rate data from CONTRACT_AND_RATES submissionType and submits successfully', async () => { - const server = await constructTestPostgresServer() + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', + rateInfos: [ + { + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 'fakeS3URL', + documentCategories: ['RATES' as const], + }, + ], + supportingDocuments: [], + rateProgramIDs: [ + '3b8d8fa1-1fa6-4504-9c5b-ef522877fe1e', + ], + actuaryContacts: [], + actuaryCommunicationPreference: + 'OACT_TO_ACTUARY' as const, + packagesWithSharedRateCerts: [], + }, + ], + }) - //Create and update a contract and rate submission to contract only with rate data - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - documentCategories: ['RATES_RELATED' as const], + const draftID = draft.id + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - ], - }) - - const draftCurrentRevision = draft.revisions[0].node - const draftPackageData = base64ToDomain( - draftCurrentRevision.formDataProto - ) + }) - if (draftPackageData instanceof Error) { - throw new Error(draftPackageData.message) - } + expect(submitResult.errors).toBeDefined() - const submitResult = await submitTestHealthPlanPackage(server, draft.id) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + expect(submitResult.errors?.[0].extensions?.code).toBe( + 'BAD_USER_INPUT' + ) + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required rate fields' + ) + }) - if (packageData instanceof Error) { - throw new Error(packageData.message) - } + it('does not remove any rate data from CONTRACT_AND_RATES submissionType and submits successfully', async () => { + const server = await constructTestPostgresServer() - expect(packageData).toEqual( - expect.objectContaining({ - addtlActuaryContacts: draftPackageData.addtlActuaryContacts, + //Create and update a contract and rate submission to contract only with rate data + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', documents: [ { name: 'contract_supporting_that_applies_to_a_rate_also.pdf', @@ -311,108 +294,126 @@ describe.each(flagValueTestParameters)( }, ], }) - ) - }) - it('removes any rate data from CONTRACT_ONLY submissionType and submits successfully', async () => { - const server = await constructTestPostgresServer() + const draftCurrentRevision = draft.revisions[0].node + const draftPackageData = base64ToDomain( + draftCurrentRevision.formDataProto + ) - //Create and update a contract and rate submission to contract only with rate data - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_ONLY', - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - documentCategories: ['RATES_RELATED' as const], - }, - ], - }) + if (draftPackageData instanceof Error) { + throw new Error(draftPackageData.message) + } - const submitResult = await submitTestHealthPlanPackage(server, draft.id) + const submitResult = await submitTestHealthPlanPackage( + server, + draft.id + ) + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + if (packageData instanceof Error) { + throw new Error(packageData.message) + } - if (packageData instanceof Error) { - throw new Error(packageData.message) - } + expect(packageData).toEqual( + expect.objectContaining({ + addtlActuaryContacts: draftPackageData.addtlActuaryContacts, + documents: [ + { + name: 'contract_supporting_that_applies_to_a_rate_also.pdf', + s3URL: 'fakeS3URL', + documentCategories: [ + 'CONTRACT_RELATED' as const, + 'RATES_RELATED' as const, + ], + }, + { + name: 'rate_only_supporting_doc.pdf', + s3URL: 'fakeS3URL', + documentCategories: ['RATES_RELATED' as const], + }, + ], + }) + ) + }) + + it('removes any rate data from CONTRACT_ONLY submissionType and submits successfully', async () => { + const server = await constructTestPostgresServer() - expect(packageData).toEqual( - expect.objectContaining({ - rateInfos: expect.arrayContaining([]), - addtlActuaryContacts: expect.arrayContaining([]), + //Create and update a contract and rate submission to contract only with rate data + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_ONLY', documents: [ { name: 'contract_supporting_that_applies_to_a_rate_also.pdf', s3URL: 'fakeS3URL', - documentCategories: ['CONTRACT_RELATED'], + documentCategories: [ + 'CONTRACT_RELATED' as const, + 'RATES_RELATED' as const, + ], }, { name: 'rate_only_supporting_doc.pdf', s3URL: 'fakeS3URL', - documentCategories: ['CONTRACT_RELATED'], + documentCategories: ['RATES_RELATED' as const], }, ], }) - ) - }) - it('removes any invalid modified provisions from CHIP submission and submits successfully', async () => { - const server = await constructTestPostgresServer() + const submitResult = await submitTestHealthPlanPackage( + server, + draft.id + ) - //Create and update a submission as if the user edited and changed population covered after filling out yes/nos - const draft = await createAndUpdateTestHealthPlanPackage(server, { - contractType: 'AMENDMENT', - populationCovered: 'CHIP', - federalAuthorities: ['TITLE_XXI'], - contractAmendmentInfo: { - modifiedProvisions: { - inLieuServicesAndSettings: true, - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: false, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: true, - modifiedWitholdAgreements: true, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: true, - modifiedPaymentsForMentalDiseaseInstitutions: true, - modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: false, - modifiedLengthOfContract: false, - modifiedNonRiskPaymentArrangements: false, - }, - }, - }) + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) + + if (packageData instanceof Error) { + throw new Error(packageData.message) + } - const submitResult = await submitTestHealthPlanPackage(server, draft.id) + expect(packageData).toEqual( + expect.objectContaining({ + rateInfos: expect.arrayContaining([]), + addtlActuaryContacts: expect.arrayContaining([]), + documents: [ + { + name: 'contract_supporting_that_applies_to_a_rate_also.pdf', + s3URL: 'fakeS3URL', + documentCategories: ['CONTRACT_RELATED'], + }, + { + name: 'rate_only_supporting_doc.pdf', + s3URL: 'fakeS3URL', + documentCategories: ['CONTRACT_RELATED'], + }, + ], + }) + ) + }) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + it('removes any invalid modified provisions from CHIP submission and submits successfully', async () => { + const server = await constructTestPostgresServer() - if (packageData instanceof Error) { - throw new Error(packageData.message) - } - expect(packageData).toEqual( - expect.objectContaining({ + //Create and update a submission as if the user edited and changed population covered after filling out yes/nos + const draft = await createAndUpdateTestHealthPlanPackage(server, { + contractType: 'AMENDMENT', + populationCovered: 'CHIP', + federalAuthorities: ['TITLE_XXI'], contractAmendmentInfo: { modifiedProvisions: { + inLieuServicesAndSettings: true, modifiedBenefitsProvided: true, modifiedGeoAreaServed: false, modifiedMedicaidBeneficiaries: false, + modifiedRiskSharingStrategy: true, + modifiedIncentiveArrangements: true, + modifiedWitholdAgreements: true, + modifiedStateDirectedPayments: true, + modifiedPassThroughPayments: true, + modifiedPaymentsForMentalDiseaseInstitutions: true, modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, modifiedEnrollmentProcess: false, modifiedGrevienceAndAppeal: false, modifiedNetworkAdequacyStandards: false, @@ -421,494 +422,539 @@ describe.each(flagValueTestParameters)( }, }, }) - ) - }) - - it('removes any invalid federal authorities from CHIP submission and submits successfully', async () => { - const server = await constructTestPostgresServer() - - //Create and update a submission as if the user edited and changed population covered after filling out yes/nos - const draft = await createAndUpdateTestHealthPlanPackage(server, { - populationCovered: 'CHIP', - federalAuthorities: [ - 'STATE_PLAN', - 'WAIVER_1915B', - 'WAIVER_1115', - 'VOLUNTARY', - 'BENCHMARK', - 'TITLE_XXI', - ], - }) - const submitResult = await submitTestHealthPlanPackage(server, draft.id) + const submitResult = await submitTestHealthPlanPackage( + server, + draft.id + ) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - if (packageData instanceof Error) { - throw new Error(packageData.message) - } - expect(packageData).toEqual( - expect.objectContaining({ - federalAuthorities: ['WAIVER_1115', 'TITLE_XXI'], + if (packageData instanceof Error) { + throw new Error(packageData.message) + } + expect(packageData).toEqual( + expect.objectContaining({ + contractAmendmentInfo: { + modifiedProvisions: { + modifiedBenefitsProvided: true, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: false, + modifiedMedicalLossRatioStandards: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: false, + modifiedNonRiskPaymentArrangements: false, + }, + }, + }) + ) + }) + + it('removes any invalid federal authorities from CHIP submission and submits successfully', async () => { + const server = await constructTestPostgresServer() + + //Create and update a submission as if the user edited and changed population covered after filling out yes/nos + const draft = await createAndUpdateTestHealthPlanPackage(server, { + populationCovered: 'CHIP', + federalAuthorities: [ + 'STATE_PLAN', + 'WAIVER_1915B', + 'WAIVER_1115', + 'VOLUNTARY', + 'BENCHMARK', + 'TITLE_XXI', + ], }) - ) - }) - it('sends two emails', async () => { - const mockEmailer = testEmailer() + const submitResult = await submitTestHealthPlanPackage( + server, + draft.id + ) - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, + if (packageData instanceof Error) { + throw new Error(packageData.message) + } + expect(packageData).toEqual( + expect.objectContaining({ + federalAuthorities: ['WAIVER_1115', 'TITLE_XXI'], + }) + ) }) - expect(submitResult.errors).toBeUndefined() - expect(mockEmailer.sendEmail).toHaveBeenCalledTimes(2) - }) + it('sends two emails', async () => { + const mockEmailer = testEmailer() + + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, + }) - it('send CMS email to CMS if submission is valid', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, + expect(submitResult.errors).toBeUndefined() + expect(mockEmailer.sendEmail).toHaveBeenCalledTimes(2) }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + it('send CMS email to CMS if submission is valid', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] + .node - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } - const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) - const stateAnalystsEmails = getTestStateAnalystsEmails(sub.stateCode) + const programs = [defaultFloridaProgram()] + const name = packageName(sub, programs) + const stateAnalystsEmails = getTestStateAnalystsEmails( + sub.stateCode + ) + + const cmsEmails = [ + ...config.devReviewTeamEmails, + ...stateAnalystsEmails, + ] + + // email subject line is correct for CMS email + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining( + `New Managed Care Submission: ${name}` + ), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining(Array.from(cmsEmails)), + }) + ) + }) + + it('does send email when request for state analysts emails fails', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const mockEmailParameterStore = mockEmailParameterStoreError() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + emailParameterStore: mockEmailParameterStore, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, + }) - const cmsEmails = [ - ...config.devReviewTeamEmails, - ...stateAnalystsEmails, - ] + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + toAddresses: expect.arrayContaining( + Array.from(config.devReviewTeamEmails) + ), + }) + ) + }) + + it('does log error when request for state specific analysts emails failed', async () => { + const mockEmailParameterStore = mockEmailParameterStoreError() + const consoleErrorSpy = jest.spyOn(console, 'error') + const error = { + error: 'No store found', + message: 'getStateAnalystsEmails failed', + operation: 'getStateAnalystsEmails', + status: 'ERROR', + } - // email subject line is correct for CMS email - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining( - `New Managed Care Submission: ${name}` - ), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining(Array.from(cmsEmails)), + const server = await constructTestPostgresServer({ + emailParameterStore: mockEmailParameterStore, }) - ) - }) - - it('does send email when request for state analysts emails fails', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const mockEmailParameterStore = mockEmailParameterStoreError() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - emailParameterStore: mockEmailParameterStore, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - - await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) - - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - toAddresses: expect.arrayContaining( - Array.from(config.devReviewTeamEmails) - ), }) - ) - }) - - it('does log error when request for state specific analysts emails failed', async () => { - const mockEmailParameterStore = mockEmailParameterStoreError() - const consoleErrorSpy = jest.spyOn(console, 'error') - const error = { - error: 'No store found', - message: 'getStateAnalystsEmails failed', - operation: 'getStateAnalystsEmails', - status: 'ERROR', - } - - const server = await constructTestPostgresServer({ - emailParameterStore: mockEmailParameterStore, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, + expect(consoleErrorSpy).toHaveBeenCalledWith(error) }) - expect(consoleErrorSpy).toHaveBeenCalledWith(error) - }) - - it('send state email to logged in user if submission is valid', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - }) + it('send state email to logged in user if submission is valid', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) - const currentUser = defaultContext().user // need this to reach into gql tests and understand who current user is - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + const currentUser = defaultContext().user // need this to reach into gql tests and understand who current user is + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) - - expect(submitResult.errors).toBeUndefined() + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + expect(submitResult.errors).toBeUndefined() - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] + .node - const programs = [defaultFloridaProgram()] - const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName(sub, programs) - const rateName = generateRateName(sub, sub.rateInfos[0], ratePrograms) + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was sent to CMS`), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining([currentUser.email]), - bodyHTML: expect.stringContaining(rateName), + const programs = [defaultFloridaProgram()] + const ratePrograms = [defaultFloridaRateProgram()] + const name = packageName(sub, programs) + const rateName = generateRateName( + sub, + sub.rateInfos[0], + ratePrograms + ) + + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was sent to CMS`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining([currentUser.email]), + bodyHTML: expect.stringContaining(rateName), + }) + ) + }) + + it('send state email to submitter if submission is valid', async () => { + const mockEmailer = testEmailer() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + context: { + user: testStateUser({ + email: 'notspiderman@example.com', + }), + }, }) - ) - }) - - it('send state email to submitter if submission is valid', async () => { - const mockEmailer = testEmailer() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - context: { - user: testStateUser({ - email: 'notspiderman@example.com', - }), - }, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) - - expect(submitResult.errors).toBeUndefined() + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + expect(submitResult.errors).toBeUndefined() - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] + .node - const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was sent to CMS`), - toAddresses: expect.arrayContaining([ - 'notspiderman@example.com', - ]), + const programs = [defaultFloridaProgram()] + const name = packageName(sub, programs) + + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was sent to CMS`), + toAddresses: expect.arrayContaining([ + 'notspiderman@example.com', + ]), + }) + ) + }) + + it('send CMS email to CMS on valid resubmission', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer({ + emailer: mockEmailer, }) - ) - }) - - it('send CMS email to CMS on valid resubmission', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - emailer: mockEmailer, - }) - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - }) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Test unlock reason.' - ) - - const submitResult = await stateServer.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - submittedReason: 'Test resubmitted reason', + const stateSubmission = await createAndSubmitTestHealthPlanPackage( + stateServer + ) + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, }, - }, - }) - - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node - - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } - - const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) - - // email subject line is correct for CMS email and contains correct email body text - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was resubmitted`), - sourceEmail: config.emailSource, - bodyText: expect.stringContaining( - `The state completed their edits on submission ${name}` - ), - toAddresses: expect.arrayContaining( - Array.from(config.devReviewTeamEmails) - ), }) - ) - }) - - it('send state email to state contacts and all submitters on valid resubmission', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ - email: 'alsonotspiderman@example.com', - }), - }, - }) - - const stateServerTwo = await constructTestPostgresServer({ - emailer: mockEmailer, - context: { - user: testStateUser({ - email: 'notspiderman@example.com', - }), - }, - }) - - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - }) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Test unlock reason.' - ) - - const submitResult = await resubmitTestHealthPlanPackage( - stateServerTwo, - stateSubmission.id, - 'Test resubmission reason' - ) - - const currentRevision = submitResult?.revisions[0].node + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Test unlock reason.' + ) + + const submitResult = await stateServer.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + submittedReason: 'Test resubmitted reason', + }, + }, + }) - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] + .node - const programs = [defaultFloridaProgram()] - const name = packageName(sub, programs) + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } - // email subject line is correct for CMS email and contains correct email body text - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was resubmitted`), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining([ - 'alsonotspiderman@example.com', - 'notspiderman@example.com', - sub.stateContacts[0].email, - ]), + const programs = [defaultFloridaProgram()] + const name = packageName(sub, programs) + + // email subject line is correct for CMS email and contains correct email body text + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was resubmitted`), + sourceEmail: config.emailSource, + bodyText: expect.stringContaining( + `The state completed their edits on submission ${name}` + ), + toAddresses: expect.arrayContaining( + Array.from(config.devReviewTeamEmails) + ), + }) + ) + }) + + it('send state email to state contacts and all submitters on valid resubmission', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ + email: 'alsonotspiderman@example.com', + }), + }, }) - ) - }) - it('does not send any emails if submission fails', async () => { - const mockEmailer = testEmailer() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', - rateInfos: [ - { - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: undefined, - rateDocuments: [], - supportingDocuments: [], - actuaryContacts: [], - packagesWithSharedRateCerts: [], + const stateServerTwo = await constructTestPostgresServer({ + emailer: mockEmailer, + context: { + user: testStateUser({ + email: 'notspiderman@example.com', + }), }, - ], - }) - const draftID = draft.id + }) - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const stateSubmission = await createAndSubmitTestHealthPlanPackage( + stateServer + ) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, }, - }, - }) + }) - expect(submitResult.errors).toBeDefined() - expect(mockEmailer.sendEmail).not.toHaveBeenCalled() - }) + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Test unlock reason.' + ) - it('errors when SES email has failed.', async () => { - const mockEmailer = testEmailer() + const submitResult = await resubmitTestHealthPlanPackage( + stateServerTwo, + stateSubmission.id, + 'Test resubmission reason' + ) - jest.spyOn(awsSESHelpers, 'testSendSESEmail').mockImplementation( - async () => { - throw new Error('Network error occurred') + const currentRevision = submitResult?.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub } - ) - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + const programs = [defaultFloridaProgram()] + const name = packageName(sub, programs) + + // email subject line is correct for CMS email and contains correct email body text + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was resubmitted`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining([ + 'alsonotspiderman@example.com', + 'notspiderman@example.com', + sub.stateContacts[0].email, + ]), + }) + ) + }) + + it('does not send any emails if submission fails', async () => { + const mockEmailer = testEmailer() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', + rateInfos: [ + { + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: undefined, + rateDocuments: [], + supportingDocuments: [], + actuaryContacts: [], + packagesWithSharedRateCerts: [], + }, + ], + }) + const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, + }) + + expect(submitResult.errors).toBeDefined() + expect(mockEmailer.sendEmail).not.toHaveBeenCalled() }) - // expect errors from submission - expect(submitResult.errors).toBeDefined() + it('errors when SES email has failed.', async () => { + const mockEmailer = testEmailer() - // expect sendEmail to have been called, so we know it did not error earlier - expect(mockEmailer.sendEmail).toHaveBeenCalled() + jest.spyOn(awsSESHelpers, 'testSendSESEmail').mockImplementation( + async () => { + throw new Error('Network error occurred') + } + ) - // expect correct graphql error. - expect(submitResult.errors?.[0]).toEqual( - expect.objectContaining({ - message: 'Email failed', - path: ['submitHealthPlanPackage'], - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'EMAIL_ERROR', - exception: { - message: 'Email failed', + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, }, }) - ) - }) - - it('errors when risk based question is undefined', async () => { - const server = await constructTestPostgresServer() - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage(server, { - riskBasedContract: undefined, + // expect errors from submission + expect(submitResult.errors).toBeDefined() + + // expect sendEmail to have been called, so we know it did not error earlier + expect(mockEmailer.sendEmail).toHaveBeenCalled() + + // expect correct graphql error. + expect(submitResult.errors?.[0]).toEqual( + expect.objectContaining({ + message: 'Email failed', + path: ['submitHealthPlanPackage'], + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + exception: { + message: 'Email failed', + }, + }, + }) + ) }) - const draft = latestFormData(initialPkg) - const draftID = draft.id - await new Promise((resolve) => setTimeout(resolve, 2000)) + it('errors when risk based question is undefined', async () => { + const server = await constructTestPostgresServer() - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + riskBasedContract: undefined, + } + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id + + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, }, - }, - }) + }) - expect(submitResult.errors).toBeDefined() - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) - }, 20000) -}) + expect(submitResult.errors).toBeDefined() + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }, 20000) + } +) describe('Feature flagged population coverage question test', () => { it('errors when population coverage question is undefined', async () => { diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts index b509aeee2e..fe0b706f05 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts @@ -37,7 +37,10 @@ import type { LockedHealthPlanFormDataType, } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { LDService } from '../../launchDarkly/launchDarkly' -import { convertContractWithRatesToFormData, convertContractWithRatesToUnlockedHPP } from '../../domain-models/contractAndRates/convertContractWithRatesToHPP' +import { + convertContractWithRatesToFormData, + convertContractWithRatesToUnlockedHPP, +} from '../../domain-models/contractAndRates/convertContractWithRatesToHPP' import type { Span } from '@opentelemetry/api' import type { PackageStatusType } from '../../domain-models/contractAndRates' @@ -49,7 +52,6 @@ type SubmissionError = { message: string } - export function isSubmissionError(err: unknown): err is SubmissionError { if (err && typeof err == 'object') { if ('code' in err && 'message' in err) { @@ -70,18 +72,20 @@ export function isSubmissionError(err: unknown): err is SubmissionError { } // Throw error if resubmitted without reason or already submitted. -const validateStatusAndUpdateInfo = (status: PackageStatusType, updateInfo: UpdateInfoType, span?: Span, submittedReason?: string) => { +const validateStatusAndUpdateInfo = ( + status: PackageStatusType, + updateInfo: UpdateInfoType, + span?: Span, + submittedReason?: string +) => { if (status === 'UNLOCKED' && submittedReason) { - updateInfo.updatedReason = submittedReason // ! destrcutive - edit the actual update info that will be attached to submission + updateInfo.updatedReason = submittedReason // !destructive - edits the actual update info attached to submission } else if (status === 'UNLOCKED' && !submittedReason) { const errMessage = 'Resubmission requires a reason' logError('submitHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new UserInputError(errMessage) - } else if ( - status === 'RESUBMITTED' || - status === 'SUBMITTED' - ) { + } else if (status === 'RESUBMITTED' || status === 'SUBMITTED') { const errMessage = `Attempted to submit an already submitted package.` logError('submitHealthPlanPackage', errMessage) throw new GraphQLError(errMessage, { @@ -171,7 +175,8 @@ export function submitHealthPlanPackageResolver( span?.setAttribute('mcreview.package_id', pkgID) let currentFormData: HealthPlanFormDataType // data from revision that is being submitted - let updatedPackage: HealthPlanPackageType // after submit + let contractRevisionID: string // id for latest contract revision - this will be passed to submit + let updatedPackage: HealthPlanPackageType // updated data from revision after submit //Set updateInfo default to initial submission const updateInfo: UpdateInfoType = { @@ -226,44 +231,54 @@ export function submitHealthPlanPackageResolver( const maybeHealthPlanPackage = convertContractWithRatesToUnlockedHPP(contractWithHistory) - if (maybeHealthPlanPackage instanceof Error) { - const errMessage = `Error convert to contractWithHistory health plan package. Message: ${maybeHealthPlanPackage.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } + if (maybeHealthPlanPackage instanceof Error) { + const errMessage = `Error convert to contractWithHistory health plan package. Message: ${maybeHealthPlanPackage.message}` + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } - // Validate user authorized to fetch state - if (contractWithHistory.stateCode !== stateFromCurrentUser) { - logError( - 'submitHealthPlanPackage', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } + // Validate user authorized to fetch state + if (contractWithHistory.stateCode !== stateFromCurrentUser) { + logError( + 'submitHealthPlanPackage', + 'user not authorized to fetch data from a different state' + ) + setErrorAttributesOnActiveSpan( + 'user not authorized to fetch data from a different state', + span + ) + throw new ForbiddenError( + 'user not authorized to fetch data from a different state' + ) + } - validateStatusAndUpdateInfo(contractWithHistory.status,updateInfo, span, submittedReason || undefined) + validateStatusAndUpdateInfo( + contractWithHistory.status, + updateInfo, + span, + submittedReason || undefined + ) // reassign variable set up before rates feature flag - currentFormData = convertContractWithRatesToFormData(contractWithHistory.revisions[0], contractWithHistory.stateCode, contractWithHistory.stateNumber) + currentFormData = convertContractWithRatesToFormData( + contractWithHistory.revisions[0], + contractWithHistory.stateCode, + contractWithHistory.stateNumber + ) + contractRevisionID = contractWithHistory.revisions[0].id } else { // fetch from package flag off - returns HealthPlanPackage const initialPackage = await store.findHealthPlanPackage( input.pkgID ) - if (isStoreError(initialPackage) || !initialPackage) { + if (isStoreError(initialPackage) || !initialPackage) { if (!initialPackage) { throw new GraphQLError('Issue finding package.', { extensions: { @@ -298,37 +313,41 @@ export function submitHealthPlanPackageResolver( }) } - // Validate user authorized to fetch state - if (initialPackage.stateCode !== stateFromCurrentUser) { - logError( - 'submitHealthPlanPackage', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } + // Validate user authorized to fetch state + if (initialPackage.stateCode !== stateFromCurrentUser) { + logError( + 'submitHealthPlanPackage', + 'user not authorized to fetch data from a different state' + ) + setErrorAttributesOnActiveSpan( + 'user not authorized to fetch data from a different state', + span + ) + throw new ForbiddenError( + 'user not authorized to fetch data from a different state' + ) + } const status = packageStatus(initialPackage) if (status instanceof Error) { - throw new GraphQLError(status.message, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'INVALID_PACKAGE_STATUS', - } + throw new GraphQLError(status.message, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'INVALID_PACKAGE_STATUS', + }, }) } - validateStatusAndUpdateInfo(status,updateInfo, span, submittedReason || undefined) + validateStatusAndUpdateInfo( + status, + updateInfo, + span, + submittedReason || undefined + ) // reassign variable set up before rates feature flag currentFormData = maybeFormData - + contractRevisionID = initialPackage.revisions[0].id } - /* Clean form data and remove fields from edits on irrelevant logic branches - CONTRACT_ONLY submission type should not contain any CONTRACT_AND_RATE rates data. @@ -366,15 +385,14 @@ export function submitHealthPlanPackageResolver( const lockedFormData = submissionResult if (ratesDatabaseRefactor) { - // Save the submitted package - const updateResult = await store.updateHealthPlanRevision( - input.pkgID, - currentFormData.id, - lockedFormData, - updateInfo - ) - if (isStoreError(updateResult)) { - const errMessage = `Issue updating a package of type ${updateResult.code}. Message: ${updateResult.message}` + // Save the contract! + const submitResult = await store.submitContract({ + contractID: contractRevisionID, + submittedByUserID: user.id, + submitReason: updateInfo.updatedReason, + }) + if (isStoreError(submitResult)) { + const errMessage = `Issue updating a package of type ${submitResult.code}. Message: ${submitResult.message}` logError('submitHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -383,14 +401,30 @@ export function submitHealthPlanPackageResolver( cause: 'DB_ERROR', }, }) + } else if (submitResult instanceof Error) { + throw new Error('Still to do - figuring out error handling') } + const maybeSubmittedPkg = + convertContractWithRatesToUnlockedHPP(submitResult) - updatedPackage = updateResult + if (maybeSubmittedPkg instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${maybeSubmittedPkg.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } + + updatedPackage = maybeSubmittedPkg } else { // Save the package! const updateResult = await store.updateHealthPlanRevision( input.pkgID, - currentFormData.id, + contractRevisionID, lockedFormData, updateInfo ) diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index 1d8770d3ba..1942c120a7 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -113,6 +113,16 @@ function mockStoreThatErrors(): Store { 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' ) }, + submitContract: async (_ID) => { + return new Error( + 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' + ) + }, + submitRate: async (_ID) => { + return new Error( + 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' + ) + }, } }