diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts index e54a8b6ed3..10ab9a12a0 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts @@ -20,11 +20,16 @@ import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' import type { FeatureFlagLDConstant, FlagValue, -} from 'app-web/src/common-code/featureFlags' +} from '../../../../app-web/src/common-code/featureFlags' import { testLDService } from '../../testHelpers/launchDarklyHelpers' -import { must } from '../../testHelpers' +import { getProgramsFromState, must } from '../../testHelpers' import { submitContract } from '../../postgres/contractAndRates/submitContract' -import type { HealthPlanFormDataType } from 'app-web/src/common-code/healthPlanFormDataType' +import type { + HealthPlanFormDataType, + RateInfoType, + StateCodeType, +} from 'app-web/src/common-code/healthPlanFormDataType' +import * as add_sha from '../../handlers/add_sha' const flagValueTestParameters: { flagName: FeatureFlagLDConstant @@ -49,6 +54,13 @@ describe.each(flagValueTestParameters)( const cmsUser = testCMSUser() const mockLDService = testLDService({ [flagName]: flagValue }) + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(add_sha, 'calculateSHA256').mockImplementation(() => { + return Promise.resolve('mockSHA256') + }) + }) + it('updates valid scalar fields in the formData', async () => { const server = await constructTestPostgresServer({ ldService: mockLDService, @@ -126,6 +138,209 @@ describe.each(flagValueTestParameters)( ) }) + it('creates, updates, and deletes rates in the contract', async () => { + const server = await constructTestPostgresServer({ + ldService: mockLDService, + }) + const createdDraft = await createTestHealthPlanPackage(server) + const ratePrograms = getProgramsFromState( + createdDraft.stateCode as StateCodeType + ) + + // Create 2 rate data for insertion + const rate1: RateInfoType = { + 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: 's3://bucketname/key/supporting-documents', + documentCategories: ['RATES' as const], + sha256: 'rate1-sha', + }, + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + 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], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + packagesWithSharedRateCerts: [], + } + + const rate2 = { + 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: 's3://bucketname/key/supporting-documents', + documentCategories: ['RATES' as const], + sha256: 'rate2-sha', + }, + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + 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], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + packagesWithSharedRateCerts: [], + } + + // update that draft form data. + const formData: HealthPlanFormDataType = Object.assign( + latestFormData(createdDraft), + { + rateInfos: [rate1, rate2], + } + ) + + // convert to base64 proto + const updatedB64 = domainToBase64(formData) + + // update the DB contract + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, + }, + }, + }) + + expect(updateResult.errors).toBeUndefined() + + const updatedHealthPlanPackage = + updateResult.data?.updateHealthPlanFormData.pkg + + const updatedFormData = latestFormData(updatedHealthPlanPackage) + + // Expect our rates to be in the contract from our database + expect(updatedFormData).toEqual( + expect.objectContaining({ + ...formData, + updatedAt: expect.any(Date), + rateInfos: expect.arrayContaining([ + expect.objectContaining({ + ...rate1, + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + expect.objectContaining({ + ...rate2, + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + ]), + }) + ) + + const rate3 = { + rateType: 'AMENDMENT' 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: [], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + 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], + actuaryContacts: [], + packagesWithSharedRateCerts: [], + } + + // Update first rate and remove second from contract and add a new rate. + const formData2: HealthPlanFormDataType = Object.assign( + latestFormData(updatedHealthPlanPackage), + { + rateInfos: [ + // updating the actuary on the first rate + { + ...updatedFormData.rateInfos[0], + actuaryContacts: [ + { + name: 'New actuary', + titleRole: 'Better title', + email: 'actuary@example.com', + actuarialFirm: 'OPTUMAS' as const, + actuarialFirmOther: '', + }, + ], + }, + { + ...rate3, + }, + ], + } + ) + + const secondUpdatedB64 = domainToBase64(formData2) + + // update the DB contract again + const updateResult2 = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: secondUpdatedB64, + }, + }, + }) + + expect(updateResult2.errors).toBeUndefined() + + const updatedHealthPlanPackage2 = + updateResult2.data?.updateHealthPlanFormData.pkg + + const updatedFormData2 = latestFormData(updatedHealthPlanPackage2) + + // Expect our rates to be updated + expect(updatedFormData2).toEqual( + expect.objectContaining({ + ...formData2, + updatedAt: expect.any(Date), + rateInfos: expect.arrayContaining([ + expect.objectContaining({ + ...formData2.rateInfos[0], + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + expect.objectContaining({ + ...formData2.rateInfos[1], + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + ]), + }) + ) + }) + it('updates relational fields such as documents and contacts', async () => { const server = await constructTestPostgresServer({ ldService: mockLDService, diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts index 44664008a8..f413661006 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts @@ -21,6 +21,11 @@ import { } from '../attributeHelper' import type { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' +import { convertSortToDomainRate } from './contractAndRates/resolverHelpers' +import type { + ContractType, + DraftContractType, +} from '../../domain-models/contractAndRates' type ProtectedFieldType = Pick< UnlockedHealthPlanFormDataType, @@ -181,38 +186,69 @@ export function updateHealthPlanFormDataResolver( throw new UserInputError(errMessage) } - // Update contract draft revision - const updateResult = await store.updateDraftContract({ - contractID: input.pkgID, - formData: { - ...unlockedFormData, - ...unlockedFormData.contractAmendmentInfo - ?.modifiedProvisions, - managedCareEntities: unlockedFormData.managedCareEntities, - stateContacts: unlockedFormData.stateContacts, - supportingDocuments: unlockedFormData.documents.map( - (doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, - } - } - ), - contractDocuments: unlockedFormData.contractDocuments.map( - (doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, + // Check for any rate updates + const updateDraftContractRates = await convertSortToDomainRate( + contractWithHistory as DraftContractType, + unlockedFormData + ) + + if (updateDraftContractRates instanceof Error) { + const errMessage = `Error converting rate. Message: ${updateDraftContractRates.message}` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + }, + }) + } + + const shouldUpdateRates = + updateDraftContractRates.updateRateRevisions?.length || + updateDraftContractRates.connectOrCreate?.length || + updateDraftContractRates.disconnectRates?.length + + let updateResult: ContractType | Error + + // Update if there are rates to update else update contract only + // No way of updating rates and contract data at the same time currently + if (shouldUpdateRates) { + updateResult = await store.updateDraftContractRates( + updateDraftContractRates + ) + } else { + // Update contract draft revision + updateResult = await store.updateDraftContract({ + contractID: input.pkgID, + formData: { + ...unlockedFormData, + ...unlockedFormData.contractAmendmentInfo + ?.modifiedProvisions, + managedCareEntities: + unlockedFormData.managedCareEntities, + stateContacts: unlockedFormData.stateContacts, + supportingDocuments: unlockedFormData.documents.map( + (doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } } - } - ), - // TODO - can add rate fields here when updateHPP handles rates as well - }, - }) + ), + contractDocuments: + unlockedFormData.contractDocuments.map((doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + }), + }, + }) + } if (updateResult instanceof Error) { const errMessage = `Error updating form data: ${input.pkgID}:: ${updateResult.message}`