diff --git a/docs/technical-design/contract-rate-refactor-relationships.md b/docs/technical-design/contract-rate-refactor-relationships.md index 83d1197684..adfa05d82c 100644 --- a/docs/technical-design/contract-rate-refactor-relationships.md +++ b/docs/technical-design/contract-rate-refactor-relationships.md @@ -52,11 +52,10 @@ submitContract unlockContract ``` -Plus a couple functions for inspecting them: +Plus a function for inspecting them: ``` findContractWithHistory -findDraftContract ``` The life cycle functions are used as follows: diff --git a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts index 1b251072b8..26e170c57f 100644 --- a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts @@ -70,6 +70,9 @@ const contractSchema = z.object({ status: z.union([z.literal('SUBMITTED'), z.literal('DRAFT')]), stateCode: z.string(), stateNumber: z.number().min(1), + // If this contract is in a DRAFT or UNLOCKED status, there will be a draftRevision + draftRevision: contractRevisionWithRatesSchema.optional(), + // All revisions are submitted and in reverse chronological order revisions: z.array(contractRevisionWithRatesSchema), }) diff --git a/services/app-api/src/domain-models/healthPlanPackage.ts b/services/app-api/src/domain-models/healthPlanPackage.ts index 9af6d3301c..7a8220ec16 100644 --- a/services/app-api/src/domain-models/healthPlanPackage.ts +++ b/services/app-api/src/domain-models/healthPlanPackage.ts @@ -74,6 +74,11 @@ function convertContractToUnlockedHealthPlanPackage( ): HealthPlanPackageType | Error { console.info('Attempting to convert contract to health plan package') + // Since drafts come in separate on the Contract type, we push it onto the revisions before converting below + if (contract.draftRevision) { + contract.revisions.unshift(contract.draftRevision) + } + const healthPlanRevisions = convertContractRevisionToHealthPlanRevision(contract) diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts index 5e465aff74..fbc6996d1f 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts @@ -588,7 +588,7 @@ describe('findContract', () => { ) ) - // Unlock A, but don't resubmit it yet. + // Unlock contract A, but don't resubmit it yet. must( await unlockContract( client, @@ -598,6 +598,17 @@ describe('findContract', () => { ) ) + // Draft should pull revision 2.0 out + const draftPreRateUnlock = must( + await findContractWithHistory(client, contractA.id) + ) + expect(draftPreRateUnlock.draftRevision).toBeDefined() + expect( + draftPreRateUnlock.draftRevision?.rateRevisions.map( + (rr) => rr.formData.rateCertificationName + ) + ).toEqual(['onepoint0', 'twopointo']) + // unlock and submit second rate rev must(await unlockRate(client, rate2.id, cmsUser.id, 'unlock for 2.1')) must( @@ -605,9 +616,33 @@ describe('findContract', () => { contractA.id, ]) ) + + // Draft should now pull draft revision 2.1 out, even though its unsubmitted + const draftPreRateSubmit = must( + await findContractWithHistory(client, contractA.id) + ) + expect(draftPreRateSubmit.draftRevision).toBeDefined() + expect( + draftPreRateSubmit.draftRevision?.rateRevisions.map( + (rr) => rr.formData.rateCertificationName + ) + ).toEqual(['onepoint0', 'twopointone']) + + // Submit Rate 2.1 must(await submitRate(client, rate2.id, stateUser.id, '2.1 update')) - // submit A1, now, should show up as a single new rev and have the latest rates + // raft should still pull revision 2.1 out + const draftPostRateSubmit = must( + await findContractWithHistory(client, contractA.id) + ) + expect(draftPostRateSubmit.draftRevision).toBeDefined() + expect( + draftPostRateSubmit.draftRevision?.rateRevisions.map( + (rr) => rr.formData.rateCertificationName + ) + ).toEqual(['onepoint0', 'twopointone']) + + // submit contract A1, now, should show up as a single new rev and have the latest rates must( await submitContract( client, diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts index 13934cff45..9e9f42808a 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts @@ -1,8 +1,8 @@ import type { PrismaTransactionType } from '../prismaTypes' import type { ContractType } from '../../domain-models/contractAndRates' import { NotFoundError } from '../storeError' -import { includeUpdateInfo } from './prismaSharedContractRateHelpers' import { parseContractWithHistory } from './parseContractWithHistory' +import { includeFullContract } from './prismaSubmittedContractHelpers' // findContractWithHistory returns a ContractType with a full set of // ContractRevisions in reverse chronological order. Each revision is a change to this @@ -17,38 +17,7 @@ async function findContractWithHistory( where: { id: contractID, }, - include: { - revisions: { - orderBy: { - createdAt: 'asc', - }, - include: { - submitInfo: includeUpdateInfo, - unlockInfo: includeUpdateInfo, - rateRevisions: { - include: { - rateRevision: { - include: { - rateDocuments: true, - supportingDocuments: true, - certifyingActuaryContacts: true, - addtlActuaryContacts: true, - submitInfo: includeUpdateInfo, - unlockInfo: includeUpdateInfo, - draftContracts: true, - }, - }, - }, - orderBy: { - validAfter: 'asc', - }, - }, - stateContacts: true, - contractDocuments: true, - supportingDocuments: true, - }, - }, - }, + include: includeFullContract, }) if (!contract) { diff --git a/services/app-api/src/postgres/contractAndRates/findDraftContract.test.ts b/services/app-api/src/postgres/contractAndRates/findDraftContract.test.ts deleted file mode 100644 index 79fedf3cd7..0000000000 --- a/services/app-api/src/postgres/contractAndRates/findDraftContract.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' -import { v4 as uuidv4 } from 'uuid' -import { submitContract } from './submitContract' -import { submitRate } from './submitRate' -import { insertDraftContract } from './insertContract' -import { unlockContract } from './unlockContract' -import { updateDraftContract } from './updateDraftContract' -import { insertDraftRate } from './insertRate' -import { updateDraftRate } from './updateDraftRate' -import { unlockRate } from './unlockRate' -import { findDraftContract } from './findDraftContract' -import { must, createInsertContractData } from '../../testHelpers' - -describe('findDraftContract', () => { - it('handles drafts correctly', 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', - }, - }) - - // Add 2 rates 1, 2 - const rate1 = must( - await insertDraftRate(client, { - stateCode: 'MN', - rateCertificationName: 'onepoint0', - }) - ) - must(await updateDraftRate(client, rate1.id, 'onepoint0', [])) - must(await submitRate(client, rate1.id, stateUser.id, 'Rate Submit')) - - const rate2 = must( - await insertDraftRate(client, { - stateCode: 'MN', - rateCertificationName: 'twopointo', - }) - ) - must(await updateDraftRate(client, rate2.id, 'twopointo', [])) - must(await submitRate(client, rate2.id, stateUser.id, 'Rate Submit 2')) - - // add a draft contract that has both of them. - const draftContractData = createInsertContractData({ - submissionDescription: 'one contract', - }) - const contractA = must( - await insertDraftContract(client, draftContractData) - ) - must( - await updateDraftContract( - client, - contractA.id, - { - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'one contract', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }, - [rate1.id, rate2.id] - ) - ) - - const draft = must(await findDraftContract(client, contractA.id)) - - if (!draft) { - throw new Error('no draft returned') - } - - expect(draft).toBeDefined() - expect(draft.rateRevisions).toHaveLength(2) - }) - - it('handles multiple rate revisions correctly', 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 cmsUser = await client.user.create({ - data: { - id: uuidv4(), - givenName: 'Zuko', - familyName: 'Hotman', - email: 'zuko@example.com', - role: 'CMS_USER', - }, - }) - - // Add rate with 2 revisions - const rate1 = must( - await insertDraftRate(client, { - stateCode: 'MN', - rateCertificationName: 'onepoint0', - }) - ) - must(await updateDraftRate(client, rate1.id, 'onepoint0', [])) - must(await submitRate(client, rate1.id, stateUser.id, 'Rate Submit')) - - const rate2 = must( - await unlockRate( - client, - rate1.id, - cmsUser.id, - 'to test out multiple revisions' - ) - ) - must(await updateDraftRate(client, rate2.id, 'draft two', [])) - must(await submitRate(client, rate2.id, stateUser.id, 'Rate Submit 2')) - - must( - await unlockRate( - client, - rate1.id, - cmsUser.id, - 'to test out unlocked rates being ignored' - ) - ) - must(await updateDraftRate(client, rate2.id, 'draft three', [])) - - // add a draft contract that has both of them. - const draftContractData = createInsertContractData({ - submissionDescription: 'one contract', - }) - - const contractA = must( - await insertDraftContract(client, draftContractData) - ) - must( - await updateDraftContract( - client, - contractA.id, - { - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'one contract', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }, - [rate1.id, rate2.id] - ) - ) - - const draft = must(await findDraftContract(client, contractA.id)) - - if (!draft) { - throw new Error('no draft returned') - } - - expect(draft).toBeDefined() - expect(draft.rateRevisions).toHaveLength(1) - expect(draft.rateRevisions[0].formData.rateCertificationName).toBe( - 'draft two' - ) - }) - - it('works on a later revision', 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 cmsUser = await client.user.create({ - data: { - id: uuidv4(), - givenName: 'Zuko', - familyName: 'Hotman', - email: 'zuko@example.com', - role: 'CMS_USER', - }, - }) - - // add a draft contract that has both of them. - const draftContractData = createInsertContractData({ - submissionDescription: 'one contract', - }) - const contractA = must( - await insertDraftContract(client, draftContractData) - ) - must( - await updateDraftContract( - client, - contractA.id, - { - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'first draft', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }, - [] - ) - ) - must( - await submitContract( - client, - contractA.id, - stateUser.id, - 'First Submission' - ) - ) - - must( - await unlockContract( - client, - contractA.id, - cmsUser.id, - 'unlock to see if draft still comes' - ) - ) - must( - await updateDraftContract( - client, - contractA.id, - { - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'draft Edit', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }, - [] - ) - ) - - const draft = must(await findDraftContract(client, contractA.id)) - - if (!draft) { - throw new Error('no draft returned') - } - - expect(draft).toBeDefined() - expect(draft.rateRevisions).toHaveLength(0) - expect(draft.formData).toEqual( - expect.objectContaining({ - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'draft Edit', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }) - ) - }) -}) diff --git a/services/app-api/src/postgres/contractAndRates/findDraftContract.ts b/services/app-api/src/postgres/contractAndRates/findDraftContract.ts deleted file mode 100644 index 9017209fa3..0000000000 --- a/services/app-api/src/postgres/contractAndRates/findDraftContract.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' -import { parseDraftContractRevision } from './parseDraftContract' -import { includeDraftContractRevisionsWithDraftRates } from './prismaDraftContractHelpers' - -// findDraftContract returns a draft (if any) for the given contract. -async function findDraftContract( - client: PrismaClient, - contractID: string -): Promise { - try { - const draftContractRevision = - await client.contractRevisionTable.findFirst({ - where: { - contractID: contractID, - submitInfo: null, - }, - include: includeDraftContractRevisionsWithDraftRates, - }) - - if (!draftContractRevision) { - return undefined - } - - return parseDraftContractRevision(draftContractRevision) - } catch (err) { - console.error('PRISMA ERROR', err) - return err - } -} - -export { findDraftContract } diff --git a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts index 04c7638210..535e95539b 100644 --- a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts @@ -1,161 +1,28 @@ -import type { UpdateInfoTable, User } from '@prisma/client' import type { PrismaTransactionType } from '../prismaTypes' -import type { - RateRevisionWithContractsType, - RateType, -} from '../../domain-models/contractAndRates' -import type { - ContractRevisionTableWithFormData, - RateRevisionTableWithFormData, -} from './prismaSharedContractRateHelpers' -import { - contractFormDataToDomainModel, - convertUpdateInfoToDomainModel, - includeUpdateInfo, - rateFormDataToDomainModel, -} from './prismaSharedContractRateHelpers' - -// this is for the internal building of individual revisions -// we convert them into RateRevisons to return them -interface RateRevisionSet { - rateRev: RateRevisionTableWithFormData - submitInfo: UpdateInfoTable & { updatedBy: User } - unlockInfo: (UpdateInfoTable & { updatedBy: User }) | undefined - contractRevs: ContractRevisionTableWithFormData[] -} +import type { RateType } from '../../domain-models/contractAndRates' +import { NotFoundError } from '../storeError' +import { includeFullRate } from './prismaSubmittedRateHelpers' +import { parseRateWithHistory } from './parseRateWithHistory' async function findRateWithHistory( client: PrismaTransactionType, rateID: string ): Promise { try { - const rateRevisions = await client.rateRevisionTable.findMany({ + const rate = await client.rateTable.findFirst({ where: { - rateID: rateID, - }, - orderBy: { - createdAt: 'asc', - }, - include: { - submitInfo: includeUpdateInfo, - unlockInfo: includeUpdateInfo, - rateDocuments: true, - supportingDocuments: true, - certifyingActuaryContacts: true, - addtlActuaryContacts: true, - draftContracts: true, - contractRevisions: { - include: { - contractRevision: { - include: { - submitInfo: includeUpdateInfo, - unlockInfo: includeUpdateInfo, - stateContacts: true, - contractDocuments: true, - supportingDocuments: true, - }, - }, - }, - orderBy: { - validAfter: 'asc', - }, - }, + id: rateID, }, + include: includeFullRate, }) - // so you get all the rate revisions. each one has a bunch of contracts - // each set of contracts gets its own "revision" in the return list - // further rateRevs naturally are their own "revision" - - const allEntries: RateRevisionSet[] = [] - for (const rateRev of rateRevisions) { - // no drafts allowed - if (!rateRev.submitInfo) { - continue - } - - const initialEntry: RateRevisionSet = { - rateRev, - submitInfo: rateRev.submitInfo, - unlockInfo: rateRev.unlockInfo || undefined, - contractRevs: [], - } - - allEntries.push(initialEntry) - - let lastEntry = initialEntry - // go through every contract revision with this rate - for (const contractRev of rateRev.contractRevisions) { - if (!contractRev.contractRevision.submitInfo) { - return new Error( - 'Programming Error: a rate is associated with an unsubmitted contract' - ) - } - - // if it's from before this rate was submitted, it's there at the beginning. - if (contractRev.validAfter <= rateRev.submitInfo.updatedAt) { - if (!contractRev.isRemoval) { - initialEntry.contractRevs.push( - contractRev.contractRevision - ) - } - } else { - // if after, then it's always a new entry in the list - let lastContracts = [...lastEntry.contractRevs] - - // take out the previous contract revision this revision supersedes - lastContracts = lastContracts.filter( - (c) => - c.contractID !== - contractRev.contractRevision.contractID - ) - if (!contractRev.isRemoval) { - lastContracts.push(contractRev.contractRevision) - } - - const newRev: RateRevisionSet = { - rateRev, - submitInfo: contractRev.contractRevision.submitInfo, - unlockInfo: - contractRev.contractRevision.unlockInfo || - undefined, - contractRevs: lastContracts, - } - - lastEntry = newRev - allEntries.push(newRev) - } - } - } - - const allRevisions: RateRevisionWithContractsType[] = allEntries.map( - (entry) => ({ - id: entry.rateRev.id, - createdAt: entry.rateRev.createdAt, - updatedAt: entry.rateRev.updatedAt, - submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), - unlockInfo: - entry.unlockInfo && - convertUpdateInfoToDomainModel(entry.unlockInfo), - formData: rateFormDataToDomainModel(entry.rateRev), - contractRevisions: entry.contractRevs.map((crev) => ({ - id: crev.id, - createdAt: crev.createdAt, - updatedAt: crev.updatedAt, - formData: contractFormDataToDomainModel(crev), - })), - }) - ) - - const finalRate: RateType = { - id: rateID, - status: 'SUBMITTED', - stateCode: 'MN', - stateNumber: 4, - revisions: allRevisions.reverse(), + if (!rate) { + const err = `PRISMA ERROR: Cannot find rate with id: ${rateID}` + console.error(err) + return new NotFoundError(err) } - return finalRate + return parseRateWithHistory(rate) } catch (err) { console.error('PRISMA ERROR', err) return err diff --git a/services/app-api/src/postgres/contractAndRates/index.ts b/services/app-api/src/postgres/contractAndRates/index.ts index dc67d262d5..345a0db98b 100644 --- a/services/app-api/src/postgres/contractAndRates/index.ts +++ b/services/app-api/src/postgres/contractAndRates/index.ts @@ -1,8 +1,7 @@ import type { InsertContractArgsType } from './insertContract' import { insertDraftContract } from './insertContract' import { findContractWithHistory } from './findContractWithHistory' -import { findDraftContract } from './findDraftContract' -export { insertDraftContract, findContractWithHistory, findDraftContract } +export { insertDraftContract, findContractWithHistory } export type { InsertContractArgsType } diff --git a/services/app-api/src/postgres/contractAndRates/insertContract.test.ts b/services/app-api/src/postgres/contractAndRates/insertContract.test.ts index cfec53db5b..909104b0d6 100644 --- a/services/app-api/src/postgres/contractAndRates/insertContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/insertContract.test.ts @@ -18,7 +18,7 @@ describe('insertContract', () => { ) // Expect a single contract revision - expect(draftContract.revisions).toHaveLength(1) + expect(draftContract.revisions).toHaveLength(0) // Expect draft contract to contain expected data. expect(draftContract).toEqual( @@ -27,18 +27,17 @@ describe('insertContract', () => { stateCode: 'MN', status: 'DRAFT', stateNumber: expect.any(Number), - revisions: expect.arrayContaining([ - expect.objectContaining({ - formData: expect.objectContaining({ - submissionType: 'CONTRACT_AND_RATES', - submissionDescription: 'Contract 1.0', - contractType: 'BASE', - programIDs: ['PMAP'], - populationCovered: 'MEDICAID', - riskBasedContract: false, - }), + draftRevision: expect.objectContaining({ + formData: expect.objectContaining({ + submissionType: 'CONTRACT_AND_RATES', + submissionDescription: 'Contract 1.0', + contractType: 'BASE', + programIDs: ['PMAP'], + populationCovered: 'MEDICAID', + riskBasedContract: false, }), - ]), + }), + revisions: [], }) ) }) diff --git a/services/app-api/src/postgres/contractAndRates/insertContract.ts b/services/app-api/src/postgres/contractAndRates/insertContract.ts index 00fad19465..95a4060ad7 100644 --- a/services/app-api/src/postgres/contractAndRates/insertContract.ts +++ b/services/app-api/src/postgres/contractAndRates/insertContract.ts @@ -5,8 +5,8 @@ import type { ContractType as PrismaContractType, } from '@prisma/client' import type { ContractType } from '../../domain-models/contractAndRates' -import { parseDraftContract } from './parseDraftContract' -import { includeDraftContractRevisionsWithDraftRates } from './prismaDraftContractHelpers' +import { parseContractWithHistory } from './parseContractWithHistory' +import { includeFullContract } from './prismaSubmittedContractHelpers' type InsertContractArgsType = { stateCode: string @@ -51,14 +51,10 @@ async function insertDraftContract( }, }, }, - include: { - revisions: { - include: includeDraftContractRevisionsWithDraftRates, - }, - }, + include: includeFullContract, }) - return parseDraftContract(contract) + return parseContractWithHistory(contract) }) } catch (err) { console.error('CONTRACT PRISMA ERR', err) diff --git a/services/app-api/src/postgres/contractAndRates/insertRate.ts b/services/app-api/src/postgres/contractAndRates/insertRate.ts index 57754271a8..527d0978d1 100644 --- a/services/app-api/src/postgres/contractAndRates/insertRate.ts +++ b/services/app-api/src/postgres/contractAndRates/insertRate.ts @@ -11,10 +11,8 @@ import type { RateType as DomainRateType, } from 'app-web/src/common-code/healthPlanFormDataType' import type { RateType } from '../../domain-models/contractAndRates' -import { - contractFormDataToDomainModel, - rateFormDataToDomainModel, -} from './prismaSharedContractRateHelpers' +import { parseRateWithHistory } from './parseRateWithHistory' +import { includeFullRate } from './prismaSubmittedRateHelpers' type InsertRateArgsType = { stateCode: StateCodeType @@ -101,53 +99,10 @@ async function insertDraftRate( }, }, }, - include: { - revisions: { - include: { - rateDocuments: true, - supportingDocuments: true, - certifyingActuaryContacts: true, - addtlActuaryContacts: true, - draftContracts: true, - contractRevisions: { - include: { - contractRevision: { - include: { - stateContacts: true, - contractDocuments: true, - supportingDocuments: true, - }, - }, - }, - }, - }, - }, - }, + include: includeFullRate, }) - const finalRate: RateType = { - id: rate.id, - status: 'DRAFT', - stateCode: rate.stateCode, - stateNumber: rate.stateNumber, - revisions: rate.revisions.map((rr) => ({ - id: rr.id, - createdAt: rr.createdAt, - updatedAt: rr.updatedAt, - formData: rateFormDataToDomainModel(rr), - - contractRevisions: rr.contractRevisions.map( - ({ contractRevision }) => ({ - id: contractRevision.id, - createdAt: contractRevision.createdAt, - updatedAt: contractRevision.updatedAt, - formData: - contractFormDataToDomainModel(contractRevision), - }) - ), - })), - } - return finalRate + return parseRateWithHistory(rate) }) } catch (err) { console.error('RATE PRISMA ERR', err) diff --git a/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts b/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts index 308f8dde1b..1a00067830 100644 --- a/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts +++ b/services/app-api/src/postgres/contractAndRates/parseContractAndRates.test.ts @@ -1,7 +1,3 @@ -import { - parseDraftContract, - parseDraftContractRevision, -} from './parseDraftContract' import { v4 as uuidv4 } from 'uuid' import { createContractData, @@ -9,48 +5,31 @@ import { createDraftContractData, } from '../../testHelpers/' import { parseContractWithHistory } from './parseContractWithHistory' -import type { - DraftContractRevisionTableWithRelations, - DraftContractTableWithRelations, -} from './prismaDraftContractHelpers' -import type { - ContractRevisionTableWithRates, - ContractTableWithRelations, -} from './prismaSubmittedContractHelpers' +import type { ContractTableFullPayload } from './prismaSubmittedContractHelpers' describe('parseDomainData', () => { describe('parseDraftContract', () => { it('can parse valid draft domain data with no errors', () => { const draftData = createDraftContractData() - const validatedDraft = parseDraftContract(draftData) + const validatedDraft = parseContractWithHistory(draftData) expect(validatedDraft).not.toBeInstanceOf(Error) }) const draftContractWithInvalidData: { - contract: DraftContractTableWithRelations + contract: ContractTableFullPayload testDescription: string }[] = [ - { - contract: createDraftContractData({ - revisions: [], - }), - testDescription: 'no contract revisions', - }, { contract: createDraftContractData({ stateNumber: 0, - revisions: [ - createContractRevision() as DraftContractRevisionTableWithRelations, - ], + revisions: [createContractRevision()], }), testDescription: 'undefined stateNumber', }, { contract: createDraftContractData({ stateCode: undefined, - revisions: [ - createContractRevision() as DraftContractRevisionTableWithRelations, - ], + revisions: [createContractRevision()], }), testDescription: 'invalid stateCode', }, @@ -76,7 +55,7 @@ describe('parseDomainData', () => { stateCode: 'OH', }, }, - }) as DraftContractRevisionTableWithRelations, + }), ], }), testDescription: 'invalid contract status of submitted', @@ -85,53 +64,66 @@ describe('parseDomainData', () => { test.each(draftContractWithInvalidData)( 'parseDraftContract returns an error when draft contract data is invalid: $testDescription', ({ contract }) => { - expect(parseDraftContract(contract)).toBeInstanceOf(Error) + expect(parseContractWithHistory(contract)).toBeInstanceOf(Error) } ) }) describe('parseDraftContractRevision', () => { it('cant parse valid contract revision with no errors', () => { - const contractRevision = - createContractRevision() as DraftContractRevisionTableWithRelations + const contractRevision = createContractData() expect( - parseDraftContractRevision(contractRevision) + parseContractWithHistory(contractRevision) ).not.toBeInstanceOf(Error) }) const draftContractRevisionsWithInvalidData: { - revision: DraftContractRevisionTableWithRelations + contract: ContractTableFullPayload testDescription: string }[] = [ { - revision: createContractRevision({ - submissionType: undefined, - }) as DraftContractRevisionTableWithRelations, + contract: createContractData({ + revisions: [ + createContractRevision({ + submissionType: undefined, + }), + ], + }), testDescription: 'invalid submissionType', }, { - revision: createContractRevision({ - submissionDescription: undefined, - }) as DraftContractRevisionTableWithRelations, + contract: createContractData({ + revisions: [ + createContractRevision({ + submissionDescription: undefined, + }), + ], + }), testDescription: 'invalid submissionDescription', }, { - revision: createContractRevision({ - contractType: undefined, - }) as DraftContractRevisionTableWithRelations, + contract: createContractData({ + revisions: [ + createContractRevision({ + contractType: undefined, + }), + ], + }), testDescription: 'invalid contractType', }, { - revision: createContractRevision({ - managedCareEntities: undefined, - }) as DraftContractRevisionTableWithRelations, + contract: createContractData({ + revisions: [ + createContractRevision({ + managedCareEntities: undefined, + }), + ], + }), testDescription: 'invalid managedCareEntities', }, ] test.each(draftContractRevisionsWithInvalidData)( 'parseDraftContractRevision returns an error when draft contract data is invalid: $testDescription', - ({ revision }) => { - expect(parseDraftContractRevision(revision)).toBeInstanceOf( - Error - ) + ({ contract: revision }) => { + expect(parseContractWithHistory(revision)).toBeInstanceOf(Error) } ) }) @@ -143,7 +135,7 @@ describe('parseDomainData', () => { }) const contractRevisionsWithInvalidData: { - contract: ContractTableWithRelations + contract: ContractTableFullPayload testDescription: string }[] = [ { @@ -196,7 +188,7 @@ describe('parseDomainData', () => { }, }, ], - }) as ContractRevisionTableWithRates, + }) as ContractTableFullPayload['revisions'][0], ], }), testDescription: 'unsubmitted rate', diff --git a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts index fab3d323dd..ac76afc6b6 100644 --- a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts @@ -1,29 +1,29 @@ import type { ContractType, ContractRevisionWithRatesType, + ContractRevisionType, } from '../../domain-models/contractAndRates' import { contractSchema } from '../../domain-models/contractAndRates' +import { draftContractRevToDomainModel } from './prismaDraftContractHelpers' import type { RateRevisionTableWithFormData, + ContractRevisionTableWithFormData, UpdateInfoTableWithUpdater, } from './prismaSharedContractRateHelpers' import { contractFormDataToDomainModel, convertUpdateInfoToDomainModel, - getContractStatus, ratesRevisionsToDomainModel, + getContractStatus, } from './prismaSharedContractRateHelpers' -import type { - ContractRevisionTableWithRates, - ContractTableWithRelations, -} from './prismaSubmittedContractHelpers' +import type { ContractTableFullPayload } from './prismaSubmittedContractHelpers' // parseContractWithHistory returns a ContractType with a full set of // ContractRevisions in reverse chronological order. Each revision is a change to this // Contract with submit and unlock info. Changes to the data of this contract, or changes // to the data or relations of associate revisions will all surface as new ContractRevisions function parseContractWithHistory( - contract: ContractTableWithRelations + contract: ContractTableFullPayload ): ContractType | Error { const contractWithHistory = contractWithHistoryToDomainModel(contract) @@ -48,43 +48,71 @@ function parseContractWithHistory( // ContractRevisionSet is for the internal building of individual revisions // we convert them into ContractRevisions to return them interface ContractRevisionSet { - contractRev: ContractRevisionTableWithRates + contractRev: ContractRevisionTableWithFormData submitInfo: UpdateInfoTableWithUpdater unlockInfo: UpdateInfoTableWithUpdater | undefined rateRevisions: RateRevisionTableWithFormData[] } -function contractRevToDomainModel( +function contractSetsToDomainModel( revisions: ContractRevisionSet[] ): ContractRevisionWithRatesType[] { const contractRevisions = revisions.map((entry) => ({ - id: entry.contractRev.id, - submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), - unlockInfo: entry.unlockInfo - ? convertUpdateInfoToDomainModel(entry.unlockInfo) - : undefined, - createdAt: entry.contractRev.createdAt, - updatedAt: entry.contractRev.updatedAt, - formData: contractFormDataToDomainModel(entry.contractRev), + ...contractRevisionToDomainModel(entry.contractRev), rateRevisions: ratesRevisionsToDomainModel(entry.rateRevisions), + + // override this contractRevisions's update infos with the one that caused this revision to be created. + submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), + unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), })) return contractRevisions } +function contractRevisionToDomainModel( + revision: ContractRevisionTableWithFormData +): ContractRevisionType { + return { + id: revision.id, + createdAt: revision.createdAt, + updatedAt: revision.updatedAt, + submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), + unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), + + formData: contractFormDataToDomainModel(revision), + } +} + +function contractRevisionsToDomainModels( + contractRevisions: ContractRevisionTableWithFormData[] +): ContractRevisionType[] { + return contractRevisions.map((crev) => contractRevisionToDomainModel(crev)) +} + // contractWithHistoryToDomainModel constructs a history for this particular contract including changes to all of its // revisions and all related rate revisions, including added and removed rates function contractWithHistoryToDomainModel( - contract: ContractTableWithRelations + contract: ContractTableFullPayload ): ContractType | Error { // We iterate through each contract revision in order, adding it as a revision in the history // then iterate through each of its rates, constructing a history of any rates that changed // between contract revision updates const allRevisionSets: ContractRevisionSet[] = [] const contractRevisions = contract.revisions + let draftRevision: ContractRevisionWithRatesType | undefined = undefined for (const contractRev of contractRevisions) { - // We exclude the draft from this list, use findDraftContract to get the current draft + // We set the draft revision aside, all ordered revisions are submitted if (!contractRev.submitInfo) { + if (draftRevision) { + return new Error( + 'PROGRAMMING ERROR: a contract may not have multiple drafts simultaneously. ID: ' + + contract.id + ) + } + + draftRevision = draftContractRevToDomainModel(contractRev) + + // skip the rest of the processing continue } @@ -145,15 +173,20 @@ function contractWithHistoryToDomainModel( } } - const revisions = contractRevToDomainModel(allRevisionSets).reverse() + const revisions = contractSetsToDomainModel(allRevisionSets).reverse() return { id: contract.id, status: getContractStatus(contract.revisions), stateCode: contract.stateCode, stateNumber: contract.stateNumber, + draftRevision: draftRevision, revisions: revisions, } } -export { parseContractWithHistory } +export { + parseContractWithHistory, + contractRevisionToDomainModel, + contractRevisionsToDomainModels, +} diff --git a/services/app-api/src/postgres/contractAndRates/parseDraftContract.ts b/services/app-api/src/postgres/contractAndRates/parseDraftContract.ts deleted file mode 100644 index 9ba9ad16ff..0000000000 --- a/services/app-api/src/postgres/contractAndRates/parseDraftContract.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - ContractRevisionWithRatesType, - ContractType, -} from '../../domain-models/contractAndRates' -import { - contractRevisionWithRatesSchema, - draftContractSchema, -} from '../../domain-models/contractAndRates' -import type { - DraftContractRevisionTableWithRelations, - DraftContractTableWithRelations, -} from '../../postgres/contractAndRates/prismaDraftContractHelpers' -import { - draftContractRevToDomainModel, - draftContractToDomainModel, -} from '../../postgres/contractAndRates/prismaDraftContractHelpers' - -function parseDraftContractRevision( - revision: DraftContractRevisionTableWithRelations -): ContractRevisionWithRatesType | Error { - const draftContractRevision = draftContractRevToDomainModel(revision) - const parseDraft = contractRevisionWithRatesSchema.safeParse( - draftContractRevision - ) - - if (!parseDraft.success) { - console.warn( - `ERROR: attempting to parse prisma draft contract revision failed: ${parseDraft.error}` - ) - return parseDraft.error - } - - return parseDraft.data -} - -function parseDraftContract( - contract: DraftContractTableWithRelations -): ContractType | Error { - const draftContract = draftContractToDomainModel(contract) - - const parseDraft = draftContractSchema.safeParse(draftContract) - - if (!parseDraft.success) { - console.warn( - `ERROR: attempting to parse prisma draft contract failed: ${parseDraft.error}` - ) - return parseDraft.error - } - - return parseDraft.data -} - -export { parseDraftContractRevision, parseDraftContract } diff --git a/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts new file mode 100644 index 0000000000..0c671f0929 --- /dev/null +++ b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts @@ -0,0 +1,135 @@ +import type { + RateRevisionWithContractsType, + RateType, +} from '../../domain-models/contractAndRates' +import { contractRevisionsToDomainModels } from './parseContractWithHistory' +import { draftRateRevToDomainModel } from './prismaDraftRatesHelpers' +import type { + ContractRevisionTableWithFormData, + RateRevisionTableWithFormData, + UpdateInfoTableWithUpdater, +} from './prismaSharedContractRateHelpers' +import { + convertUpdateInfoToDomainModel, + getContractStatus, + rateReivisionToDomainModel, +} from './prismaSharedContractRateHelpers' +import type { RateTableFullPayload } from './prismaSubmittedRateHelpers' + +// this is for the internal building of individual revisions +// we convert them into RateRevisons to return them +interface RateRevisionSet { + rateRev: RateRevisionTableWithFormData + submitInfo: UpdateInfoTableWithUpdater + unlockInfo: UpdateInfoTableWithUpdater | undefined + contractRevs: ContractRevisionTableWithFormData[] +} + +function rateSetsToDomainModel( + entries: RateRevisionSet[] +): RateRevisionWithContractsType[] { + const revisions = entries.map((entry) => ({ + ...rateReivisionToDomainModel(entry.rateRev), + + contractRevisions: contractRevisionsToDomainModels(entry.contractRevs), + + // override this contractRevisions's update infos with the one that caused this revision to be created. + submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), + unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), + })) + + return revisions +} + +function parseRateWithHistory(rate: RateTableFullPayload): RateType | Error { + // so you get all the rate revisions. each one has a bunch of contracts + // each set of contracts gets its own "revision" in the return list + // further rateRevs naturally are their own "revision" + + const allEntries: RateRevisionSet[] = [] + const rateRevisions = rate.revisions + let draftRevision: RateRevisionWithContractsType | undefined = undefined + for (const rateRev of rateRevisions) { + // We have already set the draft revision aside, all ordered revisions here should be submitted + if (!rateRev.submitInfo) { + if (draftRevision) { + return new Error( + 'PROGRAMMING ERROR: a rate may not have multiple drafts simultaneously. ID: ' + + rate.id + ) + } + + draftRevision = draftRateRevToDomainModel(rateRev) + + // skip the rest of the processing + continue + } + + const initialEntry: RateRevisionSet = { + rateRev, + submitInfo: rateRev.submitInfo, + unlockInfo: rateRev.unlockInfo || undefined, + contractRevs: [], + } + + allEntries.push(initialEntry) + + let lastEntry = initialEntry + // go through every contract revision with this rate + for (const contractRev of rateRev.contractRevisions) { + if (!contractRev.contractRevision.submitInfo) { + return new Error( + 'Programming Error: a rate is associated with an unsubmitted contract' + ) + } + + // if it's from before this rate was submitted, it's there at the beginning. + if ( + contractRev.contractRevision.submitInfo.updatedAt <= + rateRev.submitInfo.updatedAt + ) { + if (!contractRev.isRemoval) { + initialEntry.contractRevs.push(contractRev.contractRevision) + } + } else { + // if after, then it's always a new entry in the list + let lastContracts = [...lastEntry.contractRevs] + + // take out the previous contract revision this revision supersedes + lastContracts = lastContracts.filter( + (c) => + c.contractID !== contractRev.contractRevision.contractID + ) + if (!contractRev.isRemoval) { + lastContracts.push(contractRev.contractRevision) + } + + const newRev: RateRevisionSet = { + rateRev, + submitInfo: contractRev.contractRevision.submitInfo, + unlockInfo: + contractRev.contractRevision.unlockInfo || undefined, + contractRevs: lastContracts, + } + + lastEntry = newRev + allEntries.push(newRev) + } + } + } + + const allRevisions: RateRevisionWithContractsType[] = + rateSetsToDomainModel(allEntries) + + const finalRate: RateType = { + id: rate.id, + status: getContractStatus(rate.revisions), + stateCode: rate.stateCode, + stateNumber: rate.stateNumber, + revisions: allRevisions.reverse(), + } + + return finalRate +} + +export { parseRateWithHistory } diff --git a/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts index 4abd06e561..0aa5c6b7b0 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaDraftContractHelpers.ts @@ -1,70 +1,46 @@ -import type { ContractTable, RateTable } from '@prisma/client' +import type { Prisma } from '@prisma/client' import type { ContractRevisionWithRatesType, - ContractType, RateRevisionType, } from '../../domain-models/contractAndRates' -import type { - ContractRevisionTableWithFormData, - RateRevisionTableWithFormData, -} from './prismaSharedContractRateHelpers' import { contractFormDataToDomainModel, - getContractStatus, includeUpdateInfo, rateReivisionToDomainModel, } from './prismaSharedContractRateHelpers' +import type { ContractRevisionTableWithRates } from './prismaSubmittedContractHelpers' -// This is the include that gives us draft info -const includeDraftContractRevisionsWithDraftRates = { - stateContacts: true, - contractDocuments: true, - supportingDocuments: true, - draftRates: { +const includeDraftRates = { + revisions: { include: { - revisions: { - include: { - rateDocuments: true, - supportingDocuments: true, - certifyingActuaryContacts: true, - addtlActuaryContacts: true, - submitInfo: includeUpdateInfo, - unlockInfo: includeUpdateInfo, - draftContracts: true, - }, - where: { - submitInfoID: { not: null }, - }, - take: 1, - orderBy: { - createdAt: 'desc', - }, - }, + rateDocuments: true, + supportingDocuments: true, + certifyingActuaryContacts: true, + addtlActuaryContacts: true, + submitInfo: includeUpdateInfo, + unlockInfo: includeUpdateInfo, + }, + take: 1, + orderBy: { + createdAt: 'desc', }, }, -} as const +} satisfies Prisma.RateTableInclude -type DraftRateWithRelations = RateTable & { - revisions: RateRevisionTableWithFormData[] -} - -type DraftContractRevisionTableWithRelations = - ContractRevisionTableWithFormData & { - draftRates: DraftRateWithRelations[] - } - -type DraftContractTableWithRelations = ContractTable & { - revisions: DraftContractRevisionTableWithRelations[] -} +type DraftRatesTable = Prisma.RateTableGetPayload<{ + include: typeof includeDraftRates +}> function draftRatesToDomainModel( - draftRates: DraftRateWithRelations[] + draftRates: DraftRatesTable[] ): RateRevisionType[] { return draftRates.map((dr) => rateReivisionToDomainModel(dr.revisions[0])) } +// ------------------- + function draftContractRevToDomainModel( - revision: DraftContractRevisionTableWithRelations + revision: ContractRevisionTableWithRates ): ContractRevisionWithRatesType { return { id: revision.id, @@ -75,29 +51,4 @@ function draftContractRevToDomainModel( } } -function draftContractToDomainModel( - contract: DraftContractTableWithRelations -): ContractType { - const revisions = contract.revisions.map((cr) => - draftContractRevToDomainModel(cr) - ) - - return { - id: contract.id, - status: getContractStatus(contract.revisions), - stateCode: contract.stateCode, - stateNumber: contract.stateNumber, - revisions, - } -} - -export type { - DraftContractTableWithRelations, - DraftContractRevisionTableWithRelations, -} - -export { - includeDraftContractRevisionsWithDraftRates, - draftContractToDomainModel, - draftContractRevToDomainModel, -} +export { includeDraftRates, draftContractRevToDomainModel } diff --git a/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts new file mode 100644 index 0000000000..3afeb2942e --- /dev/null +++ b/services/app-api/src/postgres/contractAndRates/prismaDraftRatesHelpers.ts @@ -0,0 +1,54 @@ +import type { Prisma } from '@prisma/client' +import type { + ContractRevisionType, + RateRevisionWithContractsType, +} from '../../domain-models/contractAndRates' +import type { RateRevisionTableWithContracts } from './prismaSubmittedRateHelpers' +import { contractRevisionToDomainModel } from './parseContractWithHistory' +import { + includeContractFormData, + includeUpdateInfo, + rateFormDataToDomainModel, +} from './prismaSharedContractRateHelpers' + +const includeDraftContracts = { + revisions: { + include: { + ...includeContractFormData, + submitInfo: includeUpdateInfo, + unlockInfo: includeUpdateInfo, + }, + take: 1, + orderBy: { + createdAt: 'desc', + }, + }, +} satisfies Prisma.ContractTableInclude + +type DraftContractsTable = Prisma.ContractTableGetPayload<{ + include: typeof includeDraftContracts +}> + +function draftContractsToDomainModel( + draftContracts: DraftContractsTable[] +): ContractRevisionType[] { + return draftContracts.map((dc) => + contractRevisionToDomainModel(dc.revisions[0]) + ) +} + +// ----------- + +function draftRateRevToDomainModel( + revision: RateRevisionTableWithContracts +): RateRevisionWithContractsType { + return { + id: revision.id, + createdAt: revision.createdAt, + updatedAt: revision.updatedAt, + formData: rateFormDataToDomainModel(revision), + contractRevisions: draftContractsToDomainModel(revision.draftContracts), + } +} + +export { includeDraftContracts, draftRateRevToDomainModel } diff --git a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts index b2dbc587a5..b262c46285 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts @@ -1,18 +1,4 @@ -import type { - ActuaryContact, - ContractDocument, - ContractRevisionTable, - ContractSupportingDocument, - FederalAuthority, - ManagedCareEntity, - RateDocument, - RateRevisionsOnContractRevisionsTable, - RateRevisionTable, - RateSupportingDocument, - StateContact, - UpdateInfoTable, - User, -} from '@prisma/client' +import type { Prisma, UpdateInfoTable } from '@prisma/client' import type { DocumentCategoryType } from 'app-web/src/common-code/healthPlanFormDataType' import type { ContractFormDataType, @@ -22,13 +8,17 @@ import type { UpdateInfoType, } from '../../domain-models/contractAndRates' +const subincludeUpdateInfo = { + updatedBy: true, +} satisfies Prisma.UpdateInfoTableInclude + const includeUpdateInfo = { - include: { - updatedBy: true, - }, + include: subincludeUpdateInfo, } -type UpdateInfoTableWithUpdater = UpdateInfoTable & { updatedBy: User } +type UpdateInfoTableWithUpdater = Prisma.UpdateInfoTableGetPayload<{ + include: typeof subincludeUpdateInfo +}> function convertUpdateInfoToDomainModel( info?: UpdateInfoTableWithUpdater | null @@ -47,10 +37,10 @@ function convertUpdateInfoToDomainModel( // ----- function getContractStatus( - revision: Pick< - ContractRevisionTableWithFormData, - 'createdAt' | 'submitInfo' - >[] + revision: { + createdAt: Date + submitInfo: UpdateInfoTable | null + }[] ): ContractStatusType { // need to order revisions from latest to earliest const latestToEarliestRev = revision.sort( @@ -62,14 +52,19 @@ function getContractStatus( // ------ -type RateRevisionTableWithFormData = RateRevisionTable & { - submitInfo?: UpdateInfoTableWithUpdater | null - unlockInfo?: UpdateInfoTableWithUpdater | null - rateDocuments: RateDocument[] - supportingDocuments: RateSupportingDocument[] - certifyingActuaryContacts: ActuaryContact[] - addtlActuaryContacts: ActuaryContact[] -} +const includeRateFormData = { + submitInfo: includeUpdateInfo, + unlockInfo: includeUpdateInfo, + + rateDocuments: true, + supportingDocuments: true, + certifyingActuaryContacts: true, + addtlActuaryContacts: true, +} satisfies Prisma.RateRevisionTableInclude + +type RateRevisionTableWithFormData = Prisma.RateRevisionTableGetPayload<{ + include: typeof includeRateFormData +}> function rateFormDataToDomainModel( rateRevision: RateRevisionTableWithFormData @@ -136,6 +131,9 @@ function rateReivisionToDomainModel( id: revision.id, createdAt: revision.createdAt, updatedAt: revision.updatedAt, + unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), + submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), + formData: rateFormDataToDomainModel(revision), } } @@ -148,15 +146,19 @@ function ratesRevisionsToDomainModel( // ------ -type ContractRevisionTableWithFormData = ContractRevisionTable & { - submitInfo?: UpdateInfoTableWithUpdater | null - unlockInfo?: UpdateInfoTableWithUpdater | null - stateContacts: StateContact[] - contractDocuments: ContractDocument[] - supportingDocuments: ContractSupportingDocument[] - managedCareEntities: ManagedCareEntity[] - federalAuthorities: FederalAuthority[] -} +const includeContractFormData = { + unlockInfo: includeUpdateInfo, + submitInfo: includeUpdateInfo, + + stateContacts: true, + contractDocuments: true, + supportingDocuments: true, +} satisfies Prisma.ContractRevisionTableInclude + +type ContractRevisionTableWithFormData = + Prisma.ContractRevisionTableGetPayload<{ + include: typeof includeContractFormData + }> function contractFormDataToDomainModel( contractRevision: ContractRevisionTableWithFormData @@ -238,23 +240,18 @@ function contractFormDataToDomainModel( } } -// ------ - -type RateOnContractHistory = RateRevisionsOnContractRevisionsTable & { - rateRevision: RateRevisionTableWithFormData -} - // ------- export type { UpdateInfoTableWithUpdater, RateRevisionTableWithFormData, ContractRevisionTableWithFormData, - RateOnContractHistory, } export { includeUpdateInfo, + includeContractFormData, + includeRateFormData, getContractStatus, convertUpdateInfoToDomainModel, contractFormDataToDomainModel, diff --git a/services/app-api/src/postgres/contractAndRates/prismaSubmittedContractHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSubmittedContractHelpers.ts index 1de2f2612a..fb7333994e 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSubmittedContractHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSubmittedContractHelpers.ts @@ -1,15 +1,49 @@ -import type { ContractTable } from '@prisma/client' -import type { - ContractRevisionTableWithFormData, - RateOnContractHistory, +import type { Prisma } from '@prisma/client' +import { includeDraftRates } from './prismaDraftContractHelpers' +import { + includeContractFormData, + includeRateFormData, } from './prismaSharedContractRateHelpers' -type ContractRevisionTableWithRates = ContractRevisionTableWithFormData & { - rateRevisions: RateOnContractHistory[] -} +// Generated Types -type ContractTableWithRelations = ContractTable & { - revisions: ContractRevisionTableWithRates[] -} +// The include parameters for everything in a Contract. +const includeFullContract = { + revisions: { + orderBy: { + createdAt: 'asc', + }, + include: { + ...includeContractFormData, -export type { ContractTableWithRelations, ContractRevisionTableWithRates } + draftRates: { + include: includeDraftRates, + }, + + rateRevisions: { + include: { + rateRevision: { + include: includeRateFormData, + }, + }, + orderBy: { + validAfter: 'asc', + }, + }, + }, + }, +} satisfies Prisma.ContractTableInclude + +// ContractTableFullPayload is the type returned by any ContractTable find prisma query given the +// includeFullContract include: parameter. +// See https://www.prisma.io/blog/satisfies-operator-ur8ys8ccq7zb for a discussion of how +// the satisfies keyword enables the construction of this type. +type ContractTableFullPayload = Prisma.ContractTableGetPayload<{ + include: typeof includeFullContract +}> + +type ContractRevisionTableWithRates = ContractTableFullPayload['revisions'][0] + +export { includeFullContract } + +export type { ContractRevisionTableWithRates, ContractTableFullPayload } diff --git a/services/app-api/src/postgres/contractAndRates/prismaSubmittedRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSubmittedRateHelpers.ts new file mode 100644 index 0000000000..82dfcc76e8 --- /dev/null +++ b/services/app-api/src/postgres/contractAndRates/prismaSubmittedRateHelpers.ts @@ -0,0 +1,47 @@ +import type { Prisma } from '@prisma/client' +import { includeDraftContracts } from './prismaDraftRatesHelpers' +import { + includeContractFormData, + includeRateFormData, +} from './prismaSharedContractRateHelpers' + +// includeFullRate is the prisma includes block for a complete Rate +const includeFullRate = { + revisions: { + orderBy: { + createdAt: 'asc', + }, + include: { + ...includeRateFormData, + + draftContracts: { + include: includeDraftContracts, + }, + + contractRevisions: { + include: { + contractRevision: { + include: includeContractFormData, + }, + }, + orderBy: { + validAfter: 'asc', + }, + }, + }, + }, +} satisfies Prisma.RateTableInclude + +// RateTableFullPayload is the type returned by any RateTable find prisma query given the +// includeFullRate include: parameter. +// See https://www.prisma.io/blog/satisfies-operator-ur8ys8ccq7zb for a discussion of how +// the satisfies keyword enables the construction of this type. +type RateTableFullPayload = Prisma.RateTableGetPayload<{ + include: typeof includeFullRate +}> + +type RateRevisionTableWithContracts = RateTableFullPayload['revisions'][0] + +export { includeFullRate } + +export type { RateTableFullPayload, RateRevisionTableWithContracts } diff --git a/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.test.ts b/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.test.ts index 903ac8a63e..e3e4ce72ff 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.test.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.test.ts @@ -1,6 +1,5 @@ import { v4 as uuidv4 } from 'uuid' import { createContractRevision } from '../../testHelpers' -import type { DraftContractRevisionTableWithRelations } from './prismaDraftContractHelpers' import { contractFormDataToDomainModel, getContractStatus, @@ -10,10 +9,7 @@ import type { ContractRevisionTableWithRates } from './prismaSubmittedContractHe describe('prismaToDomainModel', () => { describe('contractFormDataToDomainModel', () => { it('correctly adds document categories to each document', () => { - const contractRevision: - | ContractRevisionTableWithRates - | DraftContractRevisionTableWithRelations = - createContractRevision() + const contractRevision = createContractRevision() const domainFormData = contractFormDataToDomainModel(contractRevision) @@ -66,7 +62,7 @@ describe('prismaToDomainModel', () => { revision: [ { createdAt: new Date(21, 2, 1), - submitInfo: undefined, + submitInfo: null, }, { createdAt: new Date(21, 3, 1), @@ -90,7 +86,7 @@ describe('prismaToDomainModel', () => { }, { createdAt: new Date(21, 1, 1), - submitInfo: undefined, + submitInfo: null, }, ], testDescription: 'latest revision has been submitted', @@ -120,7 +116,7 @@ describe('prismaToDomainModel', () => { }, { createdAt: new Date(21, 3, 1), - submitInfo: undefined, + submitInfo: null, }, { createdAt: new Date(21, 1, 1), diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts index 5bdeed0bd3..a01f1c281b 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts @@ -120,7 +120,7 @@ describe('submitContract', () => { ) // submit the first draft rate - must( + const rateA1 = must( await submitRate( client, rateA.id, @@ -132,7 +132,7 @@ describe('submitContract', () => { await client.rateRevisionsOnContractRevisionsTable.create({ data: { contractRevisionID: submittedContractA.revisions[0].id, - rateRevisionID: rateA.revisions[0].id, + rateRevisionID: rateA1.revisions[0].id, validAfter: new Date(), }, }) diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts index cd14b93671..f9b76662d2 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts @@ -5,7 +5,6 @@ import { insertDraftContract } from './insertContract' import { unlockContract } from './unlockContract' import { insertDraftRate } from './insertRate' import { unlockRate } from './unlockRate' -import { findDraftContract } from './findDraftContract' import { submitRate } from './submitRate' import { updateDraftContract } from './updateDraftContract' import { updateDraftRate } from './updateDraftRate' @@ -75,7 +74,11 @@ describe('unlockContract', () => { ) ) - const draftContract = must(await findDraftContract(client, contract.id)) + const fullDraftContract = must( + await findContractWithHistory(client, contract.id) + ) + + const draftContract = fullDraftContract.draftRevision if (draftContract === undefined) { throw Error('Contract data was undefined') @@ -94,10 +97,12 @@ describe('unlockContract', () => { await submitRate(client, rate.id, stateUser.id, 'Updated things') ) - const draftContractTwo = must( - await findDraftContract(client, contract.id) + const fullDraftContractTwo = must( + await findContractWithHistory(client, contract.id) ) + const draftContractTwo = fullDraftContractTwo.draftRevision + if (draftContractTwo === undefined) { throw Error('Contract data was undefined') } @@ -245,7 +250,7 @@ describe('unlockContract', () => { }) ) - // Connect draft contract to submitted rate + // Connect draft contract to draft rate must( await updateDraftContract( client, diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index ed2822ff16..a9fb0dfc8e 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -58,13 +58,6 @@ async function updateDraftContract( })), }, }, - include: { - rateRevisions: { - include: { - rateRevision: true, - }, - }, - }, }) return findContractWithHistory(client, contractID) diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 8d6347fc41..be0224cd3b 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -47,16 +47,12 @@ import { insertQuestionResponse, } from './questionResponse' import { findAllSupportedStates } from './state' -import type { - ContractRevisionWithRatesType, - ContractType, -} from '../domain-models/contractAndRates' -import type { InsertContractArgsType } from './contractAndRates' +import type { ContractType } from '../domain-models/contractAndRates' import { insertDraftContract, findContractWithHistory, - findDraftContract, } from './contractAndRates' +import type { InsertContractArgsType } from './contractAndRates' type Store = { findPrograms: ( @@ -141,10 +137,6 @@ type Store = { findContractWithHistory: ( contractID: string ) => Promise - - findDraftContract: ( - contractID: string - ) => Promise } function NewPostgresStore(client: PrismaClient): Store { @@ -205,7 +197,6 @@ function NewPostgresStore(client: PrismaClient): Store { insertDraftContract: (args) => insertDraftContract(client, args), findContractWithHistory: (args) => findContractWithHistory(client, args), - findDraftContract: (args) => findDraftContract(client, args), } } diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index 4ad57efa23..9a760686ad 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -38,10 +38,7 @@ export function fetchHealthPlanPackageResolver( // Here is where we flag finding health plan if (ratesDatabaseRefactor) { - // Health plans can be in two states Draft and Submitted, and we have 2 postgres functions for each. - // findContractWithHistory gets all submitted revisions and findDraftContract gets just the one draft revision - // We don't have function that gets all revisions including draft. So we have to call both functions when a - // contract is DRAFT. + // Fetch the full contract const contractWithHistory = await store.findContractWithHistory( input.pkgID ) @@ -68,37 +65,6 @@ export function fetchHealthPlanPackageResolver( }) } - if (contractWithHistory.status === 'DRAFT') { - const draftRevision = await store.findDraftContract(input.pkgID) - if (draftRevision instanceof Error) { - // If draft returns undefined we error because a draft submission should always have a draft revision. - const errMessage = `Issue finding a draft contract with id ${input.pkgID}. Message: ${draftRevision.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - if (draftRevision === undefined) { - const errMessage = `Issue finding a draft contract with id ${input.pkgID}. Message: Result was undefined.` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } - - // Pushing in the draft revision, so it would be first in the array of revisions. - contractWithHistory.revisions.push(draftRevision) - } - const convertedPkg = convertContractToUnlockedHealthPlanPackage(contractWithHistory) diff --git a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts index cade5056a2..1afc83af8b 100644 --- a/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts +++ b/services/app-api/src/testHelpers/contractAndRates/contractHelpers.ts @@ -3,13 +3,9 @@ import type { State } from '@prisma/client' import { must } from '../errorHelpers' import type { PrismaClient } from '@prisma/client' import { v4 as uuidv4 } from 'uuid' -import type { - DraftContractRevisionTableWithRelations, - DraftContractTableWithRelations, -} from '../../postgres/contractAndRates/prismaDraftContractHelpers' import type { ContractRevisionTableWithRates, - ContractTableWithRelations, + ContractTableFullPayload, } from '../../postgres/contractAndRates/prismaSubmittedContractHelpers' const createInsertContractData = ( @@ -47,8 +43,8 @@ const getStateRecord = async ( } const createDraftContractData = ( - contract?: Partial -): DraftContractTableWithRelations => ({ + contract?: Partial +): ContractTableFullPayload => ({ id: '24fb2a5f-6d0d-4e26-9906-4de28927c882', createdAt: new Date(), updatedAt: new Date(), @@ -58,14 +54,14 @@ const createDraftContractData = ( createContractRevision({ rateRevisions: undefined, submitInfo: null, - }) as DraftContractRevisionTableWithRelations, + }) as ContractRevisionTableWithRates, ], ...contract, }) const createContractData = ( - contract?: Partial -): ContractTableWithRelations => ({ + contract?: Partial +): ContractTableFullPayload => ({ id: '24fb2a5f-6d0d-4e26-9906-4de28927c882', createdAt: new Date(), updatedAt: new Date(), @@ -80,12 +76,8 @@ const createContractData = ( }) const createContractRevision = ( - revision?: Partial< - ContractRevisionTableWithRates | DraftContractRevisionTableWithRelations - > -): - | ContractRevisionTableWithRates - | DraftContractRevisionTableWithRelations => ({ + revision?: Partial +): ContractRevisionTableWithRates => ({ id: uuidv4(), createdAt: new Date(), updatedAt: new Date(), diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index 73338e9852..38dd6a67fa 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -107,11 +107,6 @@ function mockStoreThatErrors(): Store { 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' ) }, - findDraftContract: async (_ID) => { - return new Error( - 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' - ) - }, } }