diff --git a/services/app-api/src/domain-models/healthPlanPackage.ts b/services/app-api/src/domain-models/healthPlanPackage.ts index 82e18199af..60987020f5 100644 --- a/services/app-api/src/domain-models/healthPlanPackage.ts +++ b/services/app-api/src/domain-models/healthPlanPackage.ts @@ -126,7 +126,9 @@ function convertContractRateRevisionToHealthPlanRevision( rateProgramIDs: formData.rateProgramIDs, rateCertificationName: formData.rateCertificationName, actuaryContacts: formData.certifyingActuaryContacts?.length - ? formData.certifyingActuaryContacts + ? formData.certifyingActuaryContacts.concat( + formData.addtlActuaryContacts ?? [] + ) : [], // From the TODO in convertContractRevisionToHealthPlanRevision, these can just be set as whatever is in the // database. The frontend does not read this values. @@ -150,8 +152,6 @@ function convertContractRevisionToHealthPlanRevision( // the rate revision level. Since at update contract we are setting both fields in each rate using what the values // on the contract level, when we pull data out from our new DB model, we need to do the inverse by using, the // the first rates values. - // This will need to be updated and fixed when we figure out shared rates, because shared rates are not - // guaranteed to have the same values. const addtlActuaryCommunicationPreference = contract.draftRevision?.rateRevisions[0]?.formData ?.actuaryCommunicationPreference diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts index 7892d950ae..e4990f9234 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts @@ -11,9 +11,7 @@ import { updateDraftRate } from './updateDraftRate' import { unlockRate } from './unlockRate' import { findRateWithHistory } from './findRateWithHistory' import { must, createInsertContractData } from '../../testHelpers' -import { updateDraftContractRates } from './updateDraftContractRates' import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' -import type { DraftContractType } from '../../domain-models/contractAndRates' describe('findContract', () => { it('finds a stripped down contract with history', async () => { @@ -187,7 +185,7 @@ describe('findContract', () => { expect(testingContract.revisions).toHaveLength(7) // Make a new Contract Revision, changing the connections should show up as a single new rev. - must( + const unlockedContractA = must( await unlockContract( client, contractA.id, @@ -195,7 +193,7 @@ describe('findContract', () => { 'unlocking A.1' ) ) - const updatedDraftContract = must( + must( await updateDraftContract(client, { contractID: contractA.id, formData: { @@ -206,17 +204,14 @@ describe('findContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // Remove rate 1 and rate 2 from contract - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - disconnectRates: [rate1.id, rate2.id], + rateFormDatas: + unlockedContractA.draftRevision?.rateRevisions.filter( + (rateRevision) => + rateRevision.formData.rateID !== rate1.id && + rateRevision.formData.rateID !== rate2.id + ), }) ) - must( await submitContract( client, @@ -467,7 +462,7 @@ describe('findContract', () => { ) // Make a new Contract Revision, changing the connections should show up as a single new rev. - must( + const unlockedContractA = must( await unlockContract( client, contractA.id, @@ -475,7 +470,8 @@ describe('findContract', () => { 'unlocking A.1' ) ) - const updatedDraftContract = must( + // Remove rate 1 and rate 2 from contract + must( await updateDraftContract(client, { contractID: contractA.id, formData: { @@ -486,14 +482,12 @@ describe('findContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // Remove rate 1 and rate 2 from contract - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - disconnectRates: [rate1.id, rate2.id], + rateFormDatas: + unlockedContractA.draftRevision?.rateRevisions.filter( + (rateRevision) => + rateRevision.formData.rateID !== rate1.id && + rateRevision.formData.rateID !== rate2.id + ), }) ) @@ -592,11 +586,13 @@ describe('findContract', () => { }) const rate1 = createInsertRateData({ + id: uuidv4(), stateCode: 'MN', rateCertificationName: 'onepoint0', }) const rate2 = createInsertRateData({ + id: uuidv4(), stateCode: 'MN', rateCertificationName: 'twopoint0', }) @@ -608,7 +604,7 @@ describe('findContract', () => { const contractA = must( await insertDraftContract(client, draftContractData) ) - const updatedDraftContract = must( + const updatedDraftContractWithRates = must( await updateDraftContract(client, { contractID: contractA.id, formData: { @@ -619,14 +615,7 @@ describe('findContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // Add rate 1 and rate 2 to contract - const updatedDraftContractWithRates = must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - connectOrCreate: [rate1, rate2], + rateFormDatas: [rate1, rate2], }) ) diff --git a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts index 5c5dabe190..03f70b79ed 100644 --- a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts @@ -12,7 +12,6 @@ import { findRateWithHistory } from './findRateWithHistory' import { must, createInsertContractData } from '../../testHelpers' import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' import { findContractWithHistory } from './findContractWithHistory' -import { updateDraftContractRates } from './updateDraftContractRates' import type { DraftContractType } from '../../domain-models/contractAndRates' describe('findRate', () => { @@ -46,47 +45,30 @@ describe('findRate', () => { }) const rateA = must(await insertDraftRate(client, draftRateData)) - const submittedRateA = must( - await submitRate( - client, - rateA.id, - stateUser.id, - 'initial rate submit' + if (!rateA.draftRevision) { + throw new Error( + 'Unexpected error: draft rate is missing a draftRevision.' ) - ) + } // Add 3 contracts 1, 2, 3 pointing to rate A - - // create, update, submit contract1 const contract1 = must( await insertDraftContract(client, { stateCode: 'MN', - submissionDescription: 'onepointo', + submissionDescription: 'someurle.en', programIDs: ['13221'], submissionType: 'CONTRACT_ONLY', contractType: 'BASE', }) - ) as DraftContractType - must( - await updateDraftContractRates(client, { - draftContract: contract1, - connectOrCreate: [ - { - ...submittedRateA.revisions[0].formData, - }, - ], - }) ) must( - await submitContract( - client, - contract1.id, - stateUser.id, - 'Contract Submit' - ) + await updateDraftContract(client, { + contractID: contract1.id, + formData: { submissionDescription: 'someurle.en' }, + rateFormDatas: [rateA.draftRevision.formData], + }) ) - // create, update, submit contract2 const contract2 = must( await insertDraftContract(client, { stateCode: 'MN', @@ -95,27 +77,15 @@ describe('findRate', () => { submissionType: 'CONTRACT_ONLY', contractType: 'BASE', }) - ) as DraftContractType - must( - await updateDraftContractRates(client, { - draftContract: contract2, - connectOrCreate: [ - { - ...submittedRateA.revisions[0].formData, - }, - ], - }) ) must( - await submitContract( - client, - contract2.id, - stateUser.id, - 'ContractSubmit 2' - ) + await updateDraftContract(client, { + contractID: contract2.id, + formData: { submissionDescription: 'twopointo' }, + rateFormDatas: [rateA.draftRevision.formData], + }) ) - // create, update, submit contract3 const contract3 = must( await insertDraftContract(client, { stateCode: 'MN', @@ -124,17 +94,42 @@ describe('findRate', () => { submissionType: 'CONTRACT_ONLY', contractType: 'BASE', }) - ) as DraftContractType + ) must( - await updateDraftContractRates(client, { - draftContract: contract3, - connectOrCreate: [ - { - ...submittedRateA.revisions[0].formData, - }, - ], + await updateDraftContract(client, { + contractID: contract3.id, + formData: { submissionDescription: 'threepointo' }, + rateFormDatas: [rateA.draftRevision.formData], }) ) + + // Submit rateA + const submittedRateA = must( + await submitRate( + client, + rateA.id, + stateUser.id, + 'initial rate submit' + ) + ) + + // Submit Contract 1, 2, and 3 + must( + await submitContract( + client, + contract1.id, + stateUser.id, + 'Contract Submit' + ) + ) + must( + await submitContract( + client, + contract2.id, + stateUser.id, + 'ContractSubmit 2' + ) + ) must( await submitContract( client, @@ -145,9 +140,7 @@ describe('findRate', () => { ) // Now, find that rate and assert the history is what we expected - const threeRate = must( - await findRateWithHistory(client, submittedRateA.id) - ) + const threeRate = must(await findRateWithHistory(client, rateA.id)) if (threeRate instanceof Error) { throw threeRate } @@ -161,11 +154,21 @@ describe('findRate', () => { cmsUser.id, 'unlock for 2.1 remove' ) - ) as DraftContractType + ) must( - await updateDraftContractRates(client, { - draftContract: unlockedContract2, - disconnectRates: [submittedRateA.id], + await updateDraftContract(client, { + contractID: unlockedContract2.id, + formData: { + submissionType: 'CONTRACT_AND_RATES', + submissionDescription: 'a.2 body', + contractType: 'BASE', + populationCovered: 'MEDICAID', + riskBasedContract: false, + }, + rateFormDatas: + unlockedContract2.draftRevision?.rateRevisions.filter( + (rate) => rate.formData.rateID !== submittedRateA.id + ), }) ) must( @@ -200,16 +203,7 @@ describe('findRate', () => { await updateDraftContract(client, { contractID: unlockedContract1.id, formData: { submissionDescription: 'onepointone' }, - }) - ) - must( - await updateDraftContractRates(client, { - draftContract: unlockedContract1, - connectOrCreate: [ - { - ...submittedRateA.revisions[0].formData, - }, - ], + rateFormDatas: [rateA.draftRevision.formData], }) ) must( @@ -422,7 +416,7 @@ describe('findRate', () => { }) const contractA = must( await insertDraftContract(client, draftContractData) - ) as DraftContractType + ) must( await submitContract( client, @@ -516,6 +510,7 @@ describe('findRate', () => { 'unlocking A.0' ) ) + must( await updateDraftContract(client, { contractID: contractA.id, @@ -547,7 +542,7 @@ describe('findRate', () => { 'unlocking A.1' ) ) - const updatedDraftContract = must( + const updatedDraftContractA = must( await updateDraftContract(client, { contractID: contractA.id, formData: { @@ -559,12 +554,17 @@ describe('findRate', () => { riskBasedContract: false, }, }) - ) as DraftContractType + ) // Remove rate1 from contract must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - disconnectRates: [rate1.id], + await updateDraftContract(client, { + contractID: updatedDraftContractA.id, + formData: {}, + rateFormDatas: + updatedDraftContractA.draftRevision?.rateRevisions.filter( + (rateRevision) => + rateRevision.formData.rateID !== rate1.id + ), }) ) must( diff --git a/services/app-api/src/postgres/contractAndRates/index.ts b/services/app-api/src/postgres/contractAndRates/index.ts index 9d8d77500e..f17827c243 100644 --- a/services/app-api/src/postgres/contractAndRates/index.ts +++ b/services/app-api/src/postgres/contractAndRates/index.ts @@ -1,11 +1,9 @@ export type { InsertContractArgsType } from './insertContract' export type { UpdateContractArgsType } from './updateDraftContract' export type { ContractOrErrorArrayType } from './findAllContractsWithHistoryByState' -export type { UpdateDraftContractRatesType } from './updateDraftContractRates' export { insertDraftContract } from './insertContract' export { findContractWithHistory } from './findContractWithHistory' export { updateDraftContract } from './updateDraftContract' export { findAllContractsWithHistoryByState } from './findAllContractsWithHistoryByState' export { findAllContractsWithHistoryBySubmitInfo } from './findAllContractsWithHistoryBySubmitInfo' -export { updateDraftContractRates } from './updateDraftContractRates' diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts index 8b750d29bd..d9cbd1aa9d 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts @@ -11,8 +11,6 @@ import { updateDraftRate } from './updateDraftRate' import { submitContract } from './submitContract' import { findContractWithHistory } from './findContractWithHistory' import { must, createInsertContractData } from '../../testHelpers' -import { updateDraftContractRates } from './updateDraftContractRates' -import type { DraftContractType } from '../../domain-models/contractAndRates' describe('unlockContract', () => { it('Unlocks a rate without breaking connected draft contract', async () => { @@ -60,7 +58,7 @@ describe('unlockContract', () => { ) // Connect draft contract to submitted rate - const updatedDraftContract = must( + must( await updateDraftContract(client, { contractID: contract.id, formData: { @@ -71,18 +69,7 @@ describe('unlockContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // connect the rate - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - connectOrCreate: [ - { - ...submittedRate.revisions[0].formData, - }, - ], + rateFormDatas: [submittedRate.revisions[0].formData], }) ) @@ -93,7 +80,7 @@ describe('unlockContract', () => { const draftContract = fullDraftContract.draftRevision if (draftContract === undefined) { - throw Error('Contract data was undefined') + throw Error('Unexpect error: draft contract missing draft revision') } // Rate revision should be connected to contract @@ -122,7 +109,7 @@ describe('unlockContract', () => { const draftContractTwo = fullDraftContractTwo.draftRevision if (draftContractTwo === undefined) { - throw Error('Contract data was undefined') + throw Error('Unexpect error: draft contract missing draft revision') } // Contract should now have the latest rate revision @@ -176,7 +163,7 @@ describe('unlockContract', () => { ) // Connect draft contract to submitted rate - const updatedDraftContract = must( + must( await updateDraftContract(client, { contractID: contract.id, formData: { @@ -187,18 +174,7 @@ describe('unlockContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // connect the rate - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - connectOrCreate: [ - { - ...submittedRate.revisions[0].formData, - }, - ], + rateFormDatas: [submittedRate.revisions[0].formData], }) ) @@ -284,8 +260,14 @@ describe('unlockContract', () => { }) ) + if (!rate.draftRevision) { + throw new Error( + 'Unexpected error: draft rate is missing a draftRevision.' + ) + } + // Connect draft contract to draft rate - const updatedDraftContract = must( + must( await updateDraftContract(client, { contractID: contract.id, formData: { @@ -296,18 +278,7 @@ describe('unlockContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // connect the rate - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - connectOrCreate: [ - { - ...rate.draftRevision?.formData, - }, - ], + rateFormDatas: [rate.draftRevision?.formData], }) ) @@ -340,7 +311,7 @@ describe('unlockContract', () => { 'First unlock' ) ) - const updatedUnlockedContract = must( + must( await updateDraftContract(client, { contractID: contract.id, formData: { @@ -351,18 +322,7 @@ describe('unlockContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // connect the rate - must( - await updateDraftContractRates(client, { - draftContract: updatedUnlockedContract, - connectOrCreate: [ - { - ...rate.draftRevision?.formData, - }, - ], + rateFormDatas: [rate.draftRevision?.formData], }) ) @@ -410,8 +370,14 @@ describe('unlockContract', () => { }) ) + if (!rate.draftRevision) { + throw new Error( + 'Unexpected error: draft rate is missing a draftRevision.' + ) + } + // Connect draft contract to submitted rate - const updatedDraftContract = must( + must( await updateDraftContract(client, { contractID: contract.id, formData: { @@ -422,18 +388,7 @@ describe('unlockContract', () => { populationCovered: 'MEDICAID', riskBasedContract: false, }, - }) - ) as DraftContractType - - // connect the rate - must( - await updateDraftContractRates(client, { - draftContract: updatedDraftContract, - connectOrCreate: [ - { - ...rate.draftRevision?.formData, - }, - ], + rateFormDatas: [rate.draftRevision?.formData], }) ) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.test.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.test.ts index 32b190c85a..1751abda3c 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.test.ts @@ -5,6 +5,11 @@ import type { ContractFormEditable } from './updateDraftContract' import { updateDraftContract } from './updateDraftContract' import { PrismaClientValidationError } from '@prisma/client/runtime/library' import type { ContractType } from '@prisma/client' +import type { RateFormDataType } from '../../domain-models/contractAndRates' +import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' +import { v4 as uuidv4 } from 'uuid' +import type { RateFormEditable } from './updateDraftRate' +import { insertDraftRate } from './insertRate' describe('updateDraftContract', () => { afterEach(() => { @@ -251,4 +256,364 @@ describe('updateDraftContract', () => { // Expect a prisma error expect(draftContract).toBeInstanceOf(Error) // eventually should be PrismaClientKnownRequestError }) + + it('create, update, and disconnects many rates', async () => { + // Make a new contract + const client = await sharedTestPrismaClient() + const draftContractFormData = createInsertContractData({}) + const draftContract = must( + await insertDraftContract(client, draftContractFormData) + ) + + // Array of new rates to create + const newRates: RateFormDataType[] = [ + createInsertRateData({ + id: uuidv4(), + rateType: 'NEW', + }), + createInsertRateData({ + id: uuidv4(), + rateType: 'AMENDMENT', + }), + createInsertRateData({ + id: uuidv4(), + rateType: 'AMENDMENT', + rateCapitationType: 'RATE_CELL', + }), + ] + + // Update contract with new rates + const updatedContractWithNewRates = must( + await updateDraftContract(client, { + contractID: draftContract.id, + formData: {}, + rateFormDatas: newRates, + }) + ) + + if (!updatedContractWithNewRates.draftRevision) { + throw Error('Unexpect error: draft contract missing draft revision') + } + + const newlyCreatedRates = + updatedContractWithNewRates.draftRevision?.rateRevisions + + // Expect 3 rates + expect(newlyCreatedRates).toHaveLength(3) + + // Array of the current rates, but now with updates + const updateRateRevisionData: RateFormEditable[] = [ + { + ...newlyCreatedRates[0].formData, + rateCapitationType: 'RATE_RANGE', + rateDocuments: [ + { + name: 'Rate 1 Doc', + s3URL: 'fakeS3URL1', + sha256: 'someShaForRateDoc1', + }, + ], + rateDateStart: new Date(Date.UTC(2024, 5, 1)), + rateDateEnd: new Date(Date.UTC(2025, 5, 1)), + certifyingActuaryContacts: [ + { + name: 'Actuary Contact 1', + titleRole: 'Title', + email: 'statecontact1@example.com', + actuarialFirm: 'MERCER', + }, + ], + }, + { + ...newlyCreatedRates[1].formData, + rateCapitationType: 'RATE_RANGE', + rateDocuments: [ + { + name: 'Rate 2 Doc', + s3URL: 'fakeS3URL2', + sha256: 'someShaForRateDoc2', + }, + ], + rateDateStart: new Date(Date.UTC(2024, 8, 1)), + rateDateEnd: new Date(Date.UTC(2025, 7, 1)), + certifyingActuaryContacts: [ + { + name: 'Actuary Contact 2', + titleRole: 'Title', + email: 'statecontact2@example.com', + actuarialFirm: 'MILLIMAN', + }, + ], + }, + { + ...newlyCreatedRates[2].formData, + rateDocuments: [ + { + name: 'Rate 3 Doc', + s3URL: 'fakeS3URL3', + sha256: 'someShaForRateDoc3', + }, + ], + rateDateStart: new Date(Date.UTC(2024, 3, 1)), + rateDateEnd: new Date(Date.UTC(2025, 4, 1)), + certifyingActuaryContacts: [ + { + name: 'Actuary Contact 3', + titleRole: 'Title', + email: 'statecontact3@example.com', + actuarialFirmOther: 'Some other actuary firm', + }, + ], + }, + ] + + // Update many rates in the contract + const updatedContractRates = must( + await updateDraftContract(client, { + contractID: updatedContractWithNewRates.id, + formData: {}, + rateFormDatas: updateRateRevisionData, + }) + ) + + if (updatedContractRates.draftRevision === undefined) { + throw Error('Unexpect error: draft contract missing draft revision') + } + + const updatedRateRevisions = + updatedContractRates.draftRevision.rateRevisions + + // expect three updated rates + expect(updatedRateRevisions).toHaveLength(3) + + // expect rate data to match what we defined in the updates + expect(updatedRateRevisions).toEqual( + expect.arrayContaining([ + // expect updated rate revisions to have our defined rate revision data from updateRateRevisionData + expect.objectContaining({ + ...updatedRateRevisions[0], + formData: expect.objectContaining({ + ...updatedRateRevisions[0].formData, + ...updateRateRevisionData[0], + }), + }), + expect.objectContaining({ + ...updatedRateRevisions[1], + formData: expect.objectContaining({ + ...updatedRateRevisions[1].formData, + ...updateRateRevisionData[1], + }), + }), + expect.objectContaining({ + ...updatedRateRevisions[2], + formData: expect.objectContaining({ + ...updatedRateRevisions[2].formData, + ...updateRateRevisionData[2], + }), + }), + ]) + ) + + // Disconnect many rates in the contract + + // lets make sure we have rate ids + if ( + !updatedRateRevisions[0].formData || + !updatedRateRevisions[1].formData || + !updatedRateRevisions[2].formData + ) { + throw new Error( + 'Unexpected error. Rate revisions did not contain rate IDs' + ) + } + + // disconnect rate 3 + const contractAfterRateDisconnection = must( + await updateDraftContract(client, { + contractID: updatedContractRates.id, + formData: {}, + rateFormDatas: [ + updatedRateRevisions[0].formData, + updatedRateRevisions[1].formData, + ], + }) + ) + + // expect two rate revisions + expect( + contractAfterRateDisconnection.draftRevision?.rateRevisions + ).toHaveLength(2) + + // Create, Update and Disconnect many contracts + const contractAfterManyCrud = must( + await updateDraftContract(client, { + contractID: contractAfterRateDisconnection.id, + formData: {}, + // create two new rates + rateFormDatas: [ + createInsertRateData({ + id: uuidv4(), + rateType: 'NEW', + certifyingActuaryContacts: [ + { + name: 'New Contact', + titleRole: 'Title', + email: 'newstatecontact@example.com', + actuarialFirmOther: 'New firm', + }, + ], + }), + createInsertRateData({ + id: uuidv4(), + rateType: 'AMENDMENT', + certifyingActuaryContacts: [ + { + name: 'New Contact 2', + titleRole: 'Title', + email: 'newstatecontact2@example.com', + actuarialFirmOther: 'New firm 2', + }, + ], + }), + { + ...contractAfterRateDisconnection.draftRevision + ?.rateRevisions[0].formData, + certifyingActuaryContacts: [ + { + name: 'Actuary Contact 1 Last update', + titleRole: 'Title', + email: 'statecontact1@example.com', + actuarialFirm: 'MERCER', + }, + ], + }, + // leave out rate 2 for disconnection + ], + }) + ) + + if (contractAfterManyCrud.draftRevision === undefined) { + throw Error('Unexpect error: draft contract missing draft revision') + } + + const rateRevisionsAfterManyCrud = + contractAfterManyCrud.draftRevision?.rateRevisions + + // should expect 3 rates + expect(rateRevisionsAfterManyCrud).toHaveLength(3) + + // expect rates to have correct data + expect(rateRevisionsAfterManyCrud).toEqual( + expect.arrayContaining([ + // expect our first rate to the existing rate we updated + expect.objectContaining({ + ...rateRevisionsAfterManyCrud[0], + formData: { + ...rateRevisionsAfterManyCrud[0].formData, + certifyingActuaryContacts: [ + { + name: 'Actuary Contact 1 Last update', + titleRole: 'Title', + email: 'statecontact1@example.com', + actuarialFirm: 'MERCER', + }, + ], + }, + }), + // expect our second and third rate to be the new rates + expect.objectContaining({ + ...rateRevisionsAfterManyCrud[1], + formData: { + ...rateRevisionsAfterManyCrud[1].formData, + rateType: 'NEW', + certifyingActuaryContacts: [ + { + name: 'New Contact', + titleRole: 'Title', + email: 'newstatecontact@example.com', + actuarialFirmOther: 'New firm', + }, + ], + }, + }), + expect.objectContaining({ + ...rateRevisionsAfterManyCrud[2], + formData: { + ...rateRevisionsAfterManyCrud[2].formData, + rateType: 'AMENDMENT', + certifyingActuaryContacts: [ + { + name: 'New Contact 2', + titleRole: 'Title', + email: 'newstatecontact2@example.com', + actuarialFirmOther: 'New firm 2', + }, + ], + }, + }), + ]) + ) + }) + + it('connects existing rates to contract', async () => { + const client = await sharedTestPrismaClient() + + const draftContractFormData = createInsertContractData({}) + const draftContract = must( + await insertDraftContract(client, draftContractFormData) + ) + + // new draft rate + const draftRate = must( + await insertDraftRate( + client, + createInsertRateData({ + rateType: 'NEW', + stateCode: 'MN', + }) + ) + ) + + if (!draftRate.draftRevision) { + throw new Error( + 'Unexpected error: draft rate is missing a draftRevision.' + ) + } + + // get state data + const previousStateData = must( + await client.state.findFirst({ + where: { + stateCode: draftContract.stateCode, + }, + }) + ) + + if (!previousStateData) { + throw new Error('Unexpected error: Cannot find state record') + } + + const createDraftRateData = createInsertRateData({ + id: uuidv4(), + rateType: 'NEW', + stateCode: 'MN', + }) + + // update draft contract with rates + const updatedDraftContract = must( + await updateDraftContract(client, { + contractID: draftContract.id, + formData: {}, + rateFormDatas: [ + draftRate.draftRevision?.formData, + createDraftRateData, + ], + }) + ) + + // expect two rates connected to contract + expect(updatedDraftContract.draftRevision?.rateRevisions).toHaveLength( + 2 + ) + }) }) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index 973701f8ae..6224864e1f 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -2,14 +2,83 @@ import { findContractWithHistory } from './findContractWithHistory' import { NotFoundError } from '../storeError' import type { ContractType } from '../../domain-models/contractAndRates' import type { PrismaClient } from '@prisma/client' -import type { ContractFormDataType } from '../../domain-models/contractAndRates' +import type { + ContractFormDataType, + RateFormDataType, + RateRevisionType, +} from '../../domain-models/contractAndRates' +import type { StateCodeType } from 'app-web/src/common-code/healthPlanFormDataType' +import { includeDraftRates } from './prismaDraftContractHelpers' +import { rateRevisionToDomainModel } from './prismaSharedContractRateHelpers' +import type { RateFormEditable } from './updateDraftRate' +import { isEqualData } from '../../resolvers/healthPlanPackage/contractAndRates/resolverHelpers' type ContractFormEditable = Partial type UpdateContractArgsType = { contractID: string formData: ContractFormEditable + rateFormDatas?: RateFormDataType[] } + +const sortRatesForUpdate = ( + ratesFromDB: RateRevisionType[], + ratesFormClient: RateFormDataType[] +): { + upsertRates: RateFormEditable[] + disconnectRates: string[] +} => { + const upsertRates = [] + const disconnectRates = [] + + // Find rates to create or update + for (const rateData of ratesFormClient) { + // Find a matching rate revision id in the draftRatesFromDB array. + const matchingDBRate = ratesFromDB.find( + (dbRate) => dbRate.id === rateData.id + ) + + // If there are no matching rates we push into createRates + if (!matchingDBRate) { + upsertRates.push({ + id: rateData.id, + ...rateData, + }) + continue + } + + // If a match is found then we deep compare to figure out if we need to update. + const isRateDataEqual = isEqualData(matchingDBRate.formData, rateData) + + // If rates are not equal we then make the update + if (!isRateDataEqual) { + upsertRates.push({ + id: rateData.id, + rateID: matchingDBRate.id, + ...rateData, + }) + } + } + + // Find rates to disconnect + for (const dbRate of ratesFromDB) { + //Find a matching rate revision id in the ratesFormClient + const matchingHPPRate = ratesFormClient.find( + (convertedRate) => convertedRate.id === dbRate.id + ) + + // If convertedRateData does not contain the rate revision id from DB, we push these revisions rateID in disconnectRates + if (!matchingHPPRate && dbRate.formData.rateID) { + disconnectRates.push(dbRate.formData.rateID) + } + } + + return { + upsertRates, + disconnectRates, + } +} + // Update the given draft // * can change the set of draftRates // * set the formData @@ -17,7 +86,7 @@ async function updateDraftContract( client: PrismaClient, args: UpdateContractArgsType ): Promise { - const { contractID, formData } = args + const { contractID, formData, rateFormDatas } = args const { submissionType, submissionDescription, @@ -60,12 +129,156 @@ async function updateDraftContract( contractID: contractID, submitInfoID: null, }, + include: { + contract: true, + draftRates: { + include: includeDraftRates, + }, + }, }) if (!currentRev) { const err = `PRISMA ERROR: Cannot find the current rev to update with contract id: ${contractID}` console.error(err) return new NotFoundError(err) } + + const stateCode = currentRev.contract.stateCode as StateCodeType + const ratesFromDB = currentRev.draftRates.map((rate) => + rateRevisionToDomainModel(rate.revisions[0]) + ) + const updateRates = + rateFormDatas && sortRatesForUpdate(ratesFromDB, rateFormDatas) + + if (updateRates) { + for (const rateRevision of updateRates.upsertRates) { + // Check if the rate revision exists + // - We don't know if the rate exists in the DB we just know it's not connected to the contract. + // - toProtoBuffer gives every rate revision a UUID if there isn't one, so we cannot rely on revision + // id to know if it exists in the DB. + const currentRateRev = rateRevision.id + ? await tx.rateRevisionTable.findFirst({ + where: { + id: rateRevision.id, + }, + }) + : undefined + + // If rate revision does not exist, we need to create a new rate. + if (!currentRateRev) { + const { latestStateRateCertNumber } = + await client.state.update({ + data: { + latestStateRateCertNumber: { + increment: 1, + }, + }, + where: { + stateCode: stateCode, + }, + }) + + await client.rateTable.create({ + data: { + stateCode: stateCode, + stateNumber: latestStateRateCertNumber, + revisions: { + create: { + rateType: rateRevision.rateType, + rateCapitationType: + rateRevision.rateCapitationType, + rateDateStart: + rateRevision.rateDateStart, + rateDateEnd: rateRevision.rateDateEnd, + rateDateCertified: + rateRevision.rateDateCertified, + amendmentEffectiveDateStart: + rateRevision.amendmentEffectiveDateStart, + amendmentEffectiveDateEnd: + rateRevision.amendmentEffectiveDateEnd, + rateProgramIDs: + rateRevision.rateProgramIDs, + rateCertificationName: + rateRevision.rateCertificationName, + rateDocuments: { + create: rateRevision.rateDocuments, + }, + supportingDocuments: { + create: rateRevision.supportingDocuments, + }, + certifyingActuaryContacts: { + create: rateRevision.certifyingActuaryContacts, + }, + addtlActuaryContacts: { + create: rateRevision.addtlActuaryContacts, + }, + }, + }, + draftContractRevisions: { + connect: { + id: currentRev.id, + }, + }, + }, + }) + } else { + await tx.rateTable.update({ + where: { + id: currentRateRev.rateID, + }, + data: { + revisions: { + update: { + where: { + id: currentRateRev.id, + }, + data: { + rateType: rateRevision.rateType, + rateCapitationType: + rateRevision.rateCapitationType, + rateDateStart: + rateRevision.rateDateStart, + rateDateEnd: + rateRevision.rateDateEnd, + rateDateCertified: + rateRevision.rateDateCertified, + amendmentEffectiveDateStart: + rateRevision.amendmentEffectiveDateStart, + amendmentEffectiveDateEnd: + rateRevision.amendmentEffectiveDateEnd, + rateProgramIDs: + rateRevision.rateProgramIDs, + rateCertificationName: + rateRevision.rateCertificationName, + rateDocuments: { + deleteMany: {}, + create: rateRevision.rateDocuments, + }, + supportingDocuments: { + deleteMany: {}, + create: rateRevision.supportingDocuments, + }, + certifyingActuaryContacts: { + deleteMany: {}, + create: rateRevision.certifyingActuaryContacts, + }, + addtlActuaryContacts: { + deleteMany: {}, + create: rateRevision.addtlActuaryContacts, + }, + }, + }, + }, + draftContractRevisions: { + connect: { + id: currentRev.id, + }, + }, + }, + }) + } + } + } + // Then update resource, adjusting all simple fields and creating new linked resources for fields holding relationships to other day, await tx.contractRevisionTable.update({ where: { @@ -112,6 +325,13 @@ async function updateDraftContract( modifiedLengthOfContract, modifiedNonRiskPaymentArrangements, inLieuServicesAndSettings, + draftRates: { + disconnect: updateRates?.disconnectRates + ? updateRates.disconnectRates.map((rateID) => ({ + id: rateID, + })) + : [], + }, }, }) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.test.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.test.ts deleted file mode 100644 index 6c934221a7..0000000000 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.test.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' -import { createInsertContractData, must } from '../../testHelpers' -import { insertDraftContract } from './insertContract' -import type { InsertRateArgsType } from './insertRate' -import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' -import { updateDraftContractRates } from './updateDraftContractRates' -import type { InsertOrConnectRateArgsType } from './updateDraftContractRates' -import type { RateFormEditable } from './updateDraftRate' -import { submitRate } from './submitRate' -import { v4 as uuidv4 } from 'uuid' -import { insertDraftRate } from './insertRate' -import type { DraftContractType } from '../../domain-models/contractAndRates' - -describe('updateDraftContractRates', () => { - it('create, update, and disconnects many rates', async () => { - // Make a new contract - const client = await sharedTestPrismaClient() - const draftContractFormData = createInsertContractData({}) - const draftContract = must( - await insertDraftContract(client, draftContractFormData) - ) as DraftContractType - - // Array of new rates to create - const newRates: InsertOrConnectRateArgsType[] = [ - createInsertRateData({ - rateType: 'NEW', - }), - createInsertRateData({ - rateType: 'AMENDMENT', - }), - createInsertRateData({ - rateType: 'AMENDMENT', - rateCapitationType: 'RATE_CELL', - }), - ] - - // Update contract with new rates - const updatedContractWithNewRates = must( - await updateDraftContractRates(client, { - draftContract: draftContract, - connectOrCreate: newRates, - }) - ) as DraftContractType - - const newlyCreatedRates = - updatedContractWithNewRates.draftRevision.rateRevisions - - // Expect 3 rates - expect(newlyCreatedRates).toHaveLength(3) - - // Array of the current rates, but now with updates - const updateRateRevisionData: RateFormEditable[] = [ - { - ...newlyCreatedRates[0].formData, - rateCapitationType: 'RATE_RANGE', - rateDocuments: [ - { - name: 'Rate 1 Doc', - s3URL: 'fakeS3URL1', - sha256: 'someShaForRateDoc1', - }, - ], - rateDateStart: new Date(Date.UTC(2024, 5, 1)), - rateDateEnd: new Date(Date.UTC(2025, 5, 1)), - certifyingActuaryContacts: [ - { - name: 'Actuary Contact 1', - titleRole: 'Title', - email: 'statecontact1@example.com', - actuarialFirm: 'MERCER', - }, - ], - }, - { - ...newlyCreatedRates[1].formData, - rateCapitationType: 'RATE_RANGE', - rateDocuments: [ - { - name: 'Rate 2 Doc', - s3URL: 'fakeS3URL2', - sha256: 'someShaForRateDoc2', - }, - ], - rateDateStart: new Date(Date.UTC(2024, 8, 1)), - rateDateEnd: new Date(Date.UTC(2025, 7, 1)), - certifyingActuaryContacts: [ - { - name: 'Actuary Contact 2', - titleRole: 'Title', - email: 'statecontact2@example.com', - actuarialFirm: 'MILLIMAN', - }, - ], - }, - { - ...newlyCreatedRates[2].formData, - rateDocuments: [ - { - name: 'Rate 3 Doc', - s3URL: 'fakeS3URL3', - sha256: 'someShaForRateDoc3', - }, - ], - rateDateStart: new Date(Date.UTC(2024, 3, 1)), - rateDateEnd: new Date(Date.UTC(2025, 4, 1)), - certifyingActuaryContacts: [ - { - name: 'Actuary Contact 3', - titleRole: 'Title', - email: 'statecontact3@example.com', - actuarialFirmOther: 'Some other actuary firm', - }, - ], - }, - ] - - // Update many rates in the contract - const updatedContractRates = must( - await updateDraftContractRates(client, { - draftContract: updatedContractWithNewRates, - updateRateRevisions: updateRateRevisionData, - }) - ) as DraftContractType - - const updatedRateRevisions = - updatedContractRates.draftRevision.rateRevisions - - // expect three updated rates - expect(updatedRateRevisions).toHaveLength(3) - - // expect rate data to match what we defined in the updates - expect(updatedRateRevisions).toEqual( - expect.arrayContaining([ - // expect updated rate revisions to have our defined rate revision data from updateRateRevisionData - expect.objectContaining({ - ...updatedRateRevisions[0], - formData: expect.objectContaining({ - ...updatedRateRevisions[0].formData, - ...updateRateRevisionData[0], - }), - }), - expect.objectContaining({ - ...updatedRateRevisions[1], - formData: expect.objectContaining({ - ...updatedRateRevisions[1].formData, - ...updateRateRevisionData[1], - }), - }), - expect.objectContaining({ - ...updatedRateRevisions[2], - formData: expect.objectContaining({ - ...updatedRateRevisions[2].formData, - ...updateRateRevisionData[2], - }), - }), - ]) - ) - - // Disconnect many rates in the contract - - // lets make sure we have rate ids - if ( - !updatedRateRevisions[0].formData.rateID || - !updatedRateRevisions[1].formData.rateID || - !updatedRateRevisions[2].formData.rateID - ) { - throw new Error( - 'Unexpected error. Rate revisions did not contain rate IDs' - ) - } - - // set rate 3 for disconnection - const disconnectRates = [updatedRateRevisions[2].formData.rateID] - - // disconnect the rates - const contractAfterRateDisconnection = must( - await updateDraftContractRates(client, { - draftContract: updatedContractRates, - disconnectRates: disconnectRates, - }) - ) as DraftContractType - - // expect two rate revisions - expect( - contractAfterRateDisconnection.draftRevision?.rateRevisions - ).toHaveLength(2) - - // Create, Update and Disconnect many contracts - const contractAfterManyCrud = must( - await updateDraftContractRates(client, { - draftContract: contractAfterRateDisconnection, - // create two new rates - connectOrCreate: [ - createInsertRateData({ - rateType: 'NEW', - certifyingActuaryContacts: [ - { - name: 'New Contact', - titleRole: 'Title', - email: 'newstatecontact@example.com', - actuarialFirmOther: 'New firm', - }, - ], - }), - createInsertRateData({ - rateType: 'AMENDMENT', - certifyingActuaryContacts: [ - { - name: 'New Contact 2', - titleRole: 'Title', - email: 'newstatecontact2@example.com', - actuarialFirmOther: 'New firm 2', - }, - ], - }), - ], - // Update existing first rate - updateRateRevisions: [ - { - ...contractAfterRateDisconnection.draftRevision - ?.rateRevisions[0].formData, - certifyingActuaryContacts: [ - { - name: 'Actuary Contact 1 Last update', - titleRole: 'Title', - email: 'statecontact1@example.com', - actuarialFirm: 'MERCER', - }, - ], - }, - ], - // disconnect existing second rate - disconnectRates: [updatedRateRevisions[1].formData.rateID], - }) - ) as DraftContractType - - const rateRevisionsAfterManyCrud = - contractAfterManyCrud.draftRevision?.rateRevisions - - // should expect 3 rates - expect(rateRevisionsAfterManyCrud).toHaveLength(3) - - // expect rates to have correct data - expect(rateRevisionsAfterManyCrud).toEqual( - expect.arrayContaining([ - // expect our first rate to the existing rate we updated - expect.objectContaining({ - ...rateRevisionsAfterManyCrud[0], - formData: { - ...rateRevisionsAfterManyCrud[0].formData, - certifyingActuaryContacts: [ - { - name: 'Actuary Contact 1 Last update', - titleRole: 'Title', - email: 'statecontact1@example.com', - actuarialFirm: 'MERCER', - }, - ], - }, - }), - // expect our second and third rate to be the new rates - expect.objectContaining({ - ...rateRevisionsAfterManyCrud[1], - formData: { - ...rateRevisionsAfterManyCrud[1].formData, - rateType: 'NEW', - certifyingActuaryContacts: [ - { - name: 'New Contact', - titleRole: 'Title', - email: 'newstatecontact@example.com', - actuarialFirmOther: 'New firm', - }, - ], - }, - }), - expect.objectContaining({ - ...rateRevisionsAfterManyCrud[2], - formData: { - ...rateRevisionsAfterManyCrud[2].formData, - rateType: 'AMENDMENT', - certifyingActuaryContacts: [ - { - name: 'New Contact 2', - titleRole: 'Title', - email: 'newstatecontact2@example.com', - actuarialFirmOther: 'New firm 2', - }, - ], - }, - }), - ]) - ) - }) - - it('connects existing rates to contract', async () => { - const client = await sharedTestPrismaClient() - - const draftContractFormData = createInsertContractData({}) - const draftContract = must( - await insertDraftContract(client, draftContractFormData) - ) as DraftContractType - - // new rate - const draftRate = must( - await insertDraftRate( - client, - createInsertRateData({ - rateType: 'NEW', - stateCode: 'MN', - }) - ) - ) - - // get state data - const previousStateData = must( - await client.state.findFirst({ - where: { - stateCode: draftContract.stateCode, - }, - }) - ) - - if (!previousStateData) { - throw new Error('Unexpected error: Cannot find state record') - } - - const createDraftRateData = createInsertRateData({ - rateType: 'NEW', - stateCode: 'MN', - }) - - // update draft contract with rates - const updatedDraftContract = must( - await updateDraftContractRates(client, { - draftContract: draftContract, - connectOrCreate: [ - { - ...draftRate.draftRevision?.formData, - }, - { - ...createDraftRateData, - }, - ], - }) - ) as DraftContractType - - // expect two rates connected to contract - expect(updatedDraftContract.draftRevision.rateRevisions).toHaveLength(2) - }) - - it('errors when trying to update a submitted rate', async () => { - const client = await sharedTestPrismaClient() - - const stateUser = await client.user.create({ - data: { - id: uuidv4(), - givenName: 'Aang', - familyName: 'Avatar', - email: 'aang@example.com', - role: 'STATE_USER', - stateCode: 'NM', - }, - }) - - const draftContractFormData = createInsertContractData({}) - const draftContract = must( - await insertDraftContract(client, draftContractFormData) - ) as DraftContractType - - // new rate - const newRate: InsertRateArgsType = createInsertRateData({ - rateType: 'NEW', - }) - - // Update contract with new rates - const updatedContractWithNewRates = must( - await updateDraftContractRates(client, { - draftContract: draftContract, - connectOrCreate: [newRate], - }) - ) as DraftContractType - - const newlyCreatedRates = - updatedContractWithNewRates.draftRevision.rateRevisions - - // lets make sure we have rate ids - if (!newlyCreatedRates[0].formData.rateID) { - throw new Error( - 'Unexpected error. Rate revisions did not contain rate IDs' - ) - } - - // expect 1 rates - expect(newlyCreatedRates).toHaveLength(1) - - // submit rate - must( - await submitRate( - client, - newlyCreatedRates[0].formData.rateID, - stateUser.id, - 'Rate submit' - ) - ) - - // Update contract with submitted rate - const attemptToUpdateSubmittedRate = await updateDraftContractRates( - client, - { - draftContract: updatedContractWithNewRates, - updateRateRevisions: [ - { - ...newlyCreatedRates[0].formData, - rateType: 'AMENDMENT', - }, - ], - } - ) - - expect(attemptToUpdateSubmittedRate).toBeInstanceOf(Error) - }) -}) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts deleted file mode 100644 index 34715a7194..0000000000 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { - ContractType, - DraftContractType, -} from '../../domain-models/contractAndRates' -import { NotFoundError } from '../storeError' -import type { PrismaClient } from '@prisma/client' -import { findContractWithHistory } from './findContractWithHistory' -import type { RateFormEditable } from './updateDraftRate' - -type InsertOrConnectRateArgsType = RateFormEditable & { id?: string } - -type UpdateDraftContractRatesType = { - // Must be a draft contract. - draftContract: DraftContractType - connectOrCreate?: InsertOrConnectRateArgsType[] - updateRateRevisions?: RateFormEditable[] - disconnectRates?: string[] -} - -const VALID_UNFINDABLE_UUID = '00000000-0000-0000-0000-000000000000' - -async function updateDraftContractRates( - client: PrismaClient, - args: UpdateDraftContractRatesType -): Promise { - const { - draftContract, - connectOrCreate, - disconnectRates, - updateRateRevisions, - } = args - - try { - return await client.$transaction(async (tx) => { - // Create new rates - if (connectOrCreate) { - const result = await tx.state.findFirst({ - where: { - stateCode: draftContract.stateCode, - }, - }) - - if (!result) { - const err = `PRISMA ERROR: Cannot find state with stateCode: ${draftContract.stateCode}` - console.error(err) - return new NotFoundError(err) - } - - // Current state rate cert number - let latestStateRateCertNumber = result.latestStateRateCertNumber - - for (const rateRevision of connectOrCreate) { - await tx.rateTable.upsert({ - where: { - id: rateRevision.rateID ?? VALID_UNFINDABLE_UUID, - }, - update: { - draftContractRevisions: { - connect: { - id: draftContract.draftRevision.id, - }, - }, - }, - create: { - stateCode: draftContract.stateCode, - stateNumber: latestStateRateCertNumber, - revisions: { - create: { - rateType: rateRevision.rateType, - rateCapitationType: - rateRevision.rateCapitationType, - rateDocuments: { - create: rateRevision.rateDocuments, - }, - supportingDocuments: { - create: rateRevision.supportingDocuments, - }, - rateDateStart: rateRevision.rateDateStart, - rateDateEnd: rateRevision.rateDateEnd, - rateDateCertified: - rateRevision.rateDateCertified, - amendmentEffectiveDateStart: - rateRevision.amendmentEffectiveDateStart, - amendmentEffectiveDateEnd: - rateRevision.amendmentEffectiveDateEnd, - rateProgramIDs: rateRevision.rateProgramIDs, - rateCertificationName: - rateRevision.rateCertificationName, - certifyingActuaryContacts: { - create: rateRevision.certifyingActuaryContacts, - }, - addtlActuaryContacts: { - create: rateRevision.addtlActuaryContacts, - }, - actuaryCommunicationPreference: - rateRevision.actuaryCommunicationPreference, - }, - }, - draftContractRevisions: { - connect: { - id: draftContract.draftRevision.id, - }, - }, - }, - }) - - // If operation succeeds and passed in rateRevision data did not contain a id, then it was a create - // operation, and we need to increment latestStateRateCertNumber - if (!rateRevision.id) { - latestStateRateCertNumber++ - } - } - - // This is the number of rates we have created - const createdCount = - latestStateRateCertNumber - result.latestStateRateCertNumber - - // If we at least created one rate, we increment the count - if (createdCount >= 1) { - await tx.state.update({ - data: { - latestStateRateCertNumber: { - increment: createdCount, - }, - }, - where: { - stateCode: draftContract.stateCode, - }, - }) - } - } - - if (updateRateRevisions) { - for (const rateRevision of updateRateRevisions) { - // Make sure the rate revision is a draft revision - const currentRateRev = await tx.rateRevisionTable.findFirst( - { - where: { - id: rateRevision.id, - submitInfoID: null, - }, - } - ) - - if (!currentRateRev) { - console.error('No Draft Rev!') - return new Error('cant find a draft rev to submit') - } - - await tx.rateRevisionTable.update({ - where: { - id: rateRevision.id, - }, - data: { - rateType: rateRevision.rateType, - rateCapitationType: rateRevision.rateCapitationType, - - rateDocuments: { - deleteMany: {}, - create: rateRevision.rateDocuments, - }, - supportingDocuments: { - deleteMany: {}, - create: rateRevision.supportingDocuments, - }, - certifyingActuaryContacts: { - deleteMany: {}, - create: rateRevision.certifyingActuaryContacts, - }, - addtlActuaryContacts: { - deleteMany: {}, - create: rateRevision.addtlActuaryContacts, - }, - rateDateStart: rateRevision.rateDateStart, - rateDateEnd: rateRevision.rateDateEnd, - rateDateCertified: rateRevision.rateDateCertified, - amendmentEffectiveDateStart: - rateRevision.amendmentEffectiveDateStart, - amendmentEffectiveDateEnd: - rateRevision.amendmentEffectiveDateEnd, - rateProgramIDs: rateRevision.rateProgramIDs, - rateCertificationName: - rateRevision.rateCertificationName, - actuaryCommunicationPreference: - rateRevision.actuaryCommunicationPreference, - }, - }) - } - } - - if (disconnectRates) { - await tx.contractRevisionTable.update({ - where: { - id: draftContract.draftRevision.id, - }, - data: { - draftRates: { - disconnect: disconnectRates.map((id) => ({ id })), - }, - }, - }) - } - - // Find and return the latest contract data - return findContractWithHistory(tx, draftContract.id) - }) - } catch (err) { - console.error('Prisma error updating draft contracts rate', err) - return err - } -} - -export type { UpdateDraftContractRatesType, InsertOrConnectRateArgsType } -export { updateDraftContractRates } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 52d3605505..2c89d0b8e0 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -21,7 +21,6 @@ import type { InsertQuestionResponseArgs, StateType, } from '../domain-models' -import type { NotFoundError } from '../postgres' import { findPrograms, findStatePrograms } from '../postgres' import type { StoreError } from './storeError' import type { InsertHealthPlanPackageArgsType } from './healthPlanPackage' @@ -55,13 +54,11 @@ import { updateDraftContract, findAllContractsWithHistoryByState, findAllContractsWithHistoryBySubmitInfo, - updateDraftContractRates, } from './contractAndRates' import type { InsertContractArgsType, UpdateContractArgsType, ContractOrErrorArrayType, - UpdateDraftContractRatesType, } from './contractAndRates' type Store = { @@ -159,10 +156,6 @@ type Store = { findAllContractsWithHistoryBySubmitInfo: () => Promise< ContractOrErrorArrayType | Error > - - updateDraftContractRates: ( - args: UpdateDraftContractRatesType - ) => Promise } function NewPostgresStore(client: PrismaClient): Store { @@ -228,8 +221,6 @@ function NewPostgresStore(client: PrismaClient): Store { findAllContractsWithHistoryByState(client, args), findAllContractsWithHistoryBySubmitInfo: () => findAllContractsWithHistoryBySubmitInfo(client), - updateDraftContractRates: (args) => - updateDraftContractRates(client, args), } } diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts index 863eab6b03..dfa70a6a6d 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts @@ -1,15 +1,10 @@ -import type { - ContractOrErrorArrayType, - UpdateDraftContractRatesType, -} from '../../../postgres/contractAndRates' +import type { ContractOrErrorArrayType } from '../../../postgres/contractAndRates' import type { Span } from '@opentelemetry/api' import type { HealthPlanPackageType } from '../../../domain-models' import type { ContractType, RateFormDataType, DocumentType, - DraftContractType, - RateRevisionType, } from '../../../domain-models/contractAndRates' import { convertContractToUnlockedHealthPlanPackage } from '../../../domain-models' import { logError } from '../../../logger' @@ -75,15 +70,6 @@ const validateContractsAndConvert = ( return convertedContracts } -function isEqualData(target: object, source: object): boolean { - try { - assert.deepStrictEqual(target, source, 'Rate data not equal') - return true - } catch (e) { - return false - } -} - const convertHealthPlanPackageRateToDomain = async ( unlockedFormData: UnlockedHealthPlanFormDataType ): Promise => { @@ -106,30 +92,30 @@ const convertHealthPlanPackageRateToDomain = async ( }) ) - for (const hppRate of unlockedFormData.rateInfos) { + for (const hppRateFormData of unlockedFormData.rateInfos) { const rateDocuments = await convertHPPDocsToDomain( - hppRate.rateDocuments + hppRateFormData.rateDocuments ) const supportingDocuments = await convertHPPDocsToDomain( - hppRate.supportingDocuments + hppRateFormData.supportingDocuments ) const rate: RateFormDataType = { - id: hppRate.id, - rateType: hppRate.rateType, - rateCapitationType: hppRate.rateCapitationType, + id: hppRateFormData.id, + rateType: hppRateFormData.rateType, + rateCapitationType: hppRateFormData.rateCapitationType, rateDocuments: rateDocuments, supportingDocuments: supportingDocuments, - rateDateStart: hppRate.rateDateStart, - rateDateEnd: hppRate.rateDateEnd, - rateDateCertified: hppRate.rateDateCertified, + rateDateStart: hppRateFormData.rateDateStart, + rateDateEnd: hppRateFormData.rateDateEnd, + rateDateCertified: hppRateFormData.rateDateCertified, amendmentEffectiveDateStart: - hppRate.rateAmendmentInfo?.effectiveDateStart, + hppRateFormData.rateAmendmentInfo?.effectiveDateStart, amendmentEffectiveDateEnd: - hppRate.rateAmendmentInfo?.effectiveDateEnd, - rateProgramIDs: hppRate.rateProgramIDs, - rateCertificationName: hppRate.rateCertificationName, - certifyingActuaryContacts: hppRate.actuaryContacts, + hppRateFormData.rateAmendmentInfo?.effectiveDateEnd, + rateProgramIDs: hppRateFormData.rateProgramIDs, + rateCertificationName: hppRateFormData.rateCertificationName, + certifyingActuaryContacts: hppRateFormData.actuaryContacts, // TODO: The next two fields are not accounted for on the frontend UI. The frontend still thinks both these // fields are on the contract level. For now all rates will get their value from the contract level. addtlActuaryContacts: unlockedFormData.addtlActuaryContacts, @@ -137,8 +123,8 @@ const convertHealthPlanPackageRateToDomain = async ( unlockedFormData.addtlActuaryCommunicationPreference, // TODO: This field is set to empty array because we still need to figure out shared rates. This is MR-3568 packagesWithSharedRateCerts: [], - // packagesWithSharedRateCerts: hppRate.packagesWithSharedRateCerts && - // hppRate.packagesWithSharedRateCerts.filter(rate => (rate.packageId && rate.packageName)) as RateFormDataType['packagesWithSharedRateCerts'] + // packagesWithSharedRateCerts: hppRateFormData.packagesWithSharedRateCerts && + // hppRateFormData.packagesWithSharedRateCerts.filter(rate => (rate.packageId && rate.packageName)) as RateFormDataType['packagesWithSharedRateCerts'] } const parsedDomainData = rateFormDataSchema.safeParse(rate) @@ -153,94 +139,17 @@ const convertHealthPlanPackageRateToDomain = async ( return rates } -const convertSortToDomainRate = async ( - contractWithHistory: DraftContractType, - unlockedFormData: UnlockedHealthPlanFormDataType -): Promise => { - // convert all the HPP rates data to new domain rates data. This is RateRevisionType.formData - const convertedRatesData = await convertHealthPlanPackageRateToDomain( - unlockedFormData - ) - // All rates in the draft revision - const draftRatesFromDB: RateRevisionType[] = - contractWithHistory.draftRevision.rateRevisions - - // return error if any of the rates failed converting. - if (convertedRatesData instanceof Error) { - return convertedRatesData - } - - // now we filter - const connectOrCreate: UpdateDraftContractRatesType['connectOrCreate'] = [] - const updateRateRevisions: UpdateDraftContractRatesType['updateRateRevisions'] = - [] - const disconnectRates: UpdateDraftContractRatesType['disconnectRates'] = [] - - // Find rates to create, connect or update - convertedRatesData.forEach((rateData) => { - // If convertedRate has no revision ID, it gets pushed to connectOrCreate. In the data the ID is the revisionID. - if (!rateData.id) { - connectOrCreate.push(rateData) - return - } - - // Find a matching rate revision id in the draftRates array. We want to do this after the undefined id check for - // any edge case where id from db is also undefined. - const matchingDBRate = draftRatesFromDB.find( - (dbRate) => dbRate.id === rateData.id - ) - - // If there are no matching rates we push into connectOrCreate. This usually means there is a revision ID, but - // the rates from the contract in the DB does not have it. This could be a connection, although the handler will - // figure out if we need to create or connect. - if (!matchingDBRate) { - connectOrCreate.push(rateData) - return - } - - // If a match is found then we deep compare to figure out if we need to update. - const isRateDataEqual = isEqualData(matchingDBRate.formData, rateData) - - // If rates are not equal we then make the update - if (!isRateDataEqual) { - updateRateRevisions.push(rateData) - } - }) - - // Find rates to disconnect - draftRatesFromDB.forEach((dbRate) => { - const dbRateData = dbRate.formData - - // make sure this draftRate revision formData from the DB has revision.id and rateID - if (!dbRateData.id || !dbRateData.rateID) { - // skip because this has no required IDs - return - } - - //Find a matching rate revision id in the convertedRatesData - const matchingHPPRate = convertedRatesData.map( - (convertedRate) => convertedRate.id === dbRateData.id - ) - - // If convertedRateData does not contain the rate from DB, we push this revisions rateID in disconnectRates - if (!matchingHPPRate) { - disconnectRates.push(dbRateData.rateID) - } - }) - - // return UpdateDraftContractRatesType - - return { - draftContract: contractWithHistory, - connectOrCreate, - updateRateRevisions, - disconnectRates, +function isEqualData(target: object, source: object): boolean { + try { + assert.deepStrictEqual(target, source, 'Rate data not equal') + return true + } catch (e) { + return false } } export { validateContractsAndConvert, - convertSortToDomainRate, - isEqualData, convertHealthPlanPackageRateToDomain, + isEqualData, } diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts index f413661006..2d1997c939 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts @@ -21,11 +21,7 @@ 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' +import { convertHealthPlanPackageRateToDomain } from './contractAndRates/resolverHelpers' type ProtectedFieldType = Pick< UnlockedHealthPlanFormDataType, @@ -187,13 +183,11 @@ export function updateHealthPlanFormDataResolver( } // Check for any rate updates - const updateDraftContractRates = await convertSortToDomainRate( - contractWithHistory as DraftContractType, - unlockedFormData - ) + const updateRateFormDatas = + await convertHealthPlanPackageRateToDomain(unlockedFormData) - if (updateDraftContractRates instanceof Error) { - const errMessage = `Error converting rate. Message: ${updateDraftContractRates.message}` + if (updateRateFormDatas instanceof Error) { + const errMessage = `Error converting rate. Message: ${updateRateFormDatas.message}` logError('updateHealthPlanFormData', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -203,52 +197,38 @@ export function updateHealthPlanFormDataResolver( }) } - 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, - } + // 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, - } - }), - }, - }) - } + } + ), + contractDocuments: unlockedFormData.contractDocuments.map( + (doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + } + ), + }, + rateFormDatas: updateRateFormDatas, + }) if (updateResult instanceof Error) { const errMessage = `Error updating form data: ${input.pkgID}:: ${updateResult.message}` diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index 81d8a218d7..34a8d8f625 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -117,9 +117,6 @@ function mockStoreThatErrors(): Store { findAllContractsWithHistoryBySubmitInfo: async () => { return genericError }, - updateDraftContractRates: async (_ID) => { - return genericError - }, } }