diff --git a/services/app-api/src/common-code/featureFlags/flags.ts b/services/app-api/src/common-code/featureFlags/flags.ts index 32af51befc..61bac0122f 100644 --- a/services/app-api/src/common-code/featureFlags/flags.ts +++ b/services/app-api/src/common-code/featureFlags/flags.ts @@ -10,7 +10,7 @@ const featureFlags = { flag: '438-attestation', defaultValue: false, }, - /** + /** * Enables state and CMS rate edit, unlock, resubmit functionality */ RATE_EDIT_UNLOCK: { @@ -22,7 +22,7 @@ const featureFlags = { /** Enables the modal that alerts the user to an expiring session */ - SESSION_EXPIRING_MODAL: { + SESSION_EXPIRING_MODAL: { flag: 'session-expiring-modal', defaultValue: true, }, @@ -36,7 +36,7 @@ const featureFlags = { /** Toggles the site maintenance alert on the landing page */ - SITE_UNDER_MAINTENANCE_BANNER: { + SITE_UNDER_MAINTENANCE_BANNER: { flag: 'site-under-maintenance-banner', defaultValue: 'OFF', }, diff --git a/services/app-api/src/domain-models/contractAndRates/baseContractRateTypes.ts b/services/app-api/src/domain-models/contractAndRates/baseContractRateTypes.ts index ea86cd1845..8a53570c94 100644 --- a/services/app-api/src/domain-models/contractAndRates/baseContractRateTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/baseContractRateTypes.ts @@ -3,12 +3,7 @@ import { contractPackageSubmissionSchema, ratePackageSubmissionSchema, } from './packageSubmissions' -import { - contractRevisionSchema, - contractRevisionWithRatesSchema, - rateRevisionSchema, - rateRevisionWithContractsSchema, -} from './revisionTypes' +import { contractRevisionSchema, rateRevisionSchema } from './revisionTypes' import { statusSchema } from './statusType' import { indexQuestionsPayload } from '../QuestionsType' import { updateInfoSchema } from './updateInfoType' @@ -28,7 +23,7 @@ const contractWithoutDraftRatesSchema = z.object({ draftRevision: contractRevisionSchema.optional(), // All revisions are submitted and in reverse chronological order - revisions: z.array(contractRevisionWithRatesSchema), + revisions: z.array(contractRevisionSchema), packageSubmissions: z.array(contractPackageSubmissionSchema), @@ -50,9 +45,8 @@ const rateWithoutDraftContractsSchema = z.object({ withdrawInfo: updateInfoSchema.optional(), // If this rate is in a DRAFT or UNLOCKED status, there will be a draftRevision draftRevision: rateRevisionSchema.optional(), - // draftContracts: rateDraftContracts, // All revisions are submitted and in reverse chronological order - revisions: z.array(rateRevisionWithContractsSchema), + revisions: z.array(rateRevisionSchema), packageSubmissions: z.array(ratePackageSubmissionSchema), }) diff --git a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts index 566f369278..062f9d3170 100644 --- a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts @@ -1,8 +1,5 @@ import { z } from 'zod' -import { - contractRevisionSchema, - contractRevisionWithRatesSchema, -} from './revisionTypes' +import { contractRevisionSchema } from './revisionTypes' import { unlockedContractStatusSchema } from './statusType' import { pruneDuplicateEmails } from '../../emailer/formatters' import { @@ -23,8 +20,8 @@ const unlockedContractSchema = contractSchema.extend({ const draftContractSchema = contractSchema.extend({ status: z.literal('DRAFT'), - draftRevision: contractRevisionWithRatesSchema, - revisions: z.array(contractRevisionWithRatesSchema).min(1), + draftRevision: contractRevisionSchema, + revisions: z.array(contractRevisionSchema).min(1), }) type ContractType = z.infer @@ -43,7 +40,6 @@ function contractSubmitters(contract: ContractType): string[] { } export { - contractRevisionWithRatesSchema, unlockedContractSchema, draftContractSchema, contractSchema, diff --git a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts index 5b6434fa1d..57eb21faed 100644 --- a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts +++ b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts @@ -31,12 +31,6 @@ function convertContractToDraftRateRevisions(contract: ContractType) { function convertContractWithRatesToUnlockedHPP( contract: ContractType ): HealthPlanPackageType | Error { - // Since drafts come in separate on the Contract type, we push it onto the revisions before converting below - if (contract.draftRevision) { - const rateRevisions = convertContractToDraftRateRevisions(contract) - contract.revisions.unshift({ ...contract.draftRevision, rateRevisions }) - } - const healthPlanRevisions = convertContractWithRatesRevtoHPPRev(contract) if (healthPlanRevisions instanceof Error) { @@ -55,10 +49,51 @@ function convertContractWithRatesRevtoHPPRev( contract: ContractType ): HealthPlanRevisionType[] | Error { let healthPlanRevisions: HealthPlanRevisionType[] | Error = [] - for (const contractRev of contract.revisions) { + + if (contract.draftRevision && contract.draftRates) { + const contractRev = contract.draftRevision + const rateRevisions = convertContractToDraftRateRevisions(contract) + + const unlockedHealthPlanFormData = convertContractWithRatesToFormData( + contractRev, + rateRevisions, + contract.id, + contract.stateCode, + contract.stateNumber + ) + + if (unlockedHealthPlanFormData instanceof Error) { + return unlockedHealthPlanFormData + } + + const formDataProto = toProtoBuffer(unlockedHealthPlanFormData) + + const healthPlanRevision: HealthPlanRevisionType = { + id: contractRev.id, + unlockInfo: contractRev.unlockInfo, + submitInfo: contractRev.submitInfo, + createdAt: contractRev.createdAt, + formDataProto, + } + + healthPlanRevisions.push(healthPlanRevision) + } + + // Convert a list of package submissions into a list of pkg revisions + let lastSeenContractRevID: string | undefined = undefined + for (const submission of contract.packageSubmissions) { + const contractRev = submission.contractRevision + if (contractRev.id === lastSeenContractRevID) { + continue + } + + // otherwise, this is a new contract rev so we add a new rev to the history. + // this is lossy, our HPP revs don't map 1:1 to package submissions. Temporary. + lastSeenContractRevID = contractRev.id + const unlockedHealthPlanFormData = convertContractWithRatesToFormData( contractRev, - contractRev.rateRevisions, + submission.rateRevisions, contract.id, contract.stateCode, contract.stateNumber diff --git a/services/app-api/src/domain-models/contractAndRates/index.ts b/services/app-api/src/domain-models/contractAndRates/index.ts index eb4bc6530e..b3ad5d509c 100644 --- a/services/app-api/src/domain-models/contractAndRates/index.ts +++ b/services/app-api/src/domain-models/contractAndRates/index.ts @@ -9,12 +9,7 @@ export { export { contractFormDataSchema, rateFormDataSchema } from './formDataTypes' -export { - rateRevisionWithContractsSchema, - contractRevisionWithRatesSchema, - contractRevisionSchema, - rateRevisionSchema, -} from './revisionTypes' +export { contractRevisionSchema, rateRevisionSchema } from './revisionTypes' export { statusSchema } from './statusType' @@ -37,12 +32,7 @@ export type { ContractFormEditableType, } from './formDataTypes' -export type { - ContractRevisionType, - RateRevisionType, - RateRevisionWithContractsType, - ContractRevisionWithRatesType, -} from './revisionTypes' +export type { ContractRevisionType, RateRevisionType } from './revisionTypes' export type { ContractPackageSubmissionType, diff --git a/services/app-api/src/domain-models/contractAndRates/rateTypes.ts b/services/app-api/src/domain-models/contractAndRates/rateTypes.ts index 484f362323..2ef86e2d0d 100644 --- a/services/app-api/src/domain-models/contractAndRates/rateTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/rateTypes.ts @@ -3,10 +3,7 @@ import { contractWithoutDraftRatesSchema, rateWithoutDraftContractsSchema, } from './baseContractRateTypes' -import { - rateRevisionWithContractsSchema, - rateRevisionSchema, -} from './revisionTypes' +import { rateRevisionSchema } from './revisionTypes' const rateSchema = rateWithoutDraftContractsSchema.extend({ draftContracts: z.array(contractWithoutDraftRatesSchema).optional(), @@ -14,6 +11,6 @@ const rateSchema = rateWithoutDraftContractsSchema.extend({ type RateType = z.infer -export { rateRevisionSchema, rateRevisionWithContractsSchema, rateSchema } +export { rateRevisionSchema, rateSchema } export type { RateType } diff --git a/services/app-api/src/domain-models/contractAndRates/revisionTypes.ts b/services/app-api/src/domain-models/contractAndRates/revisionTypes.ts index b8c078f8d6..46e0c16b46 100644 --- a/services/app-api/src/domain-models/contractAndRates/revisionTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/revisionTypes.ts @@ -26,33 +26,9 @@ const rateRevisionSchema = z.object({ formData: rateFormDataSchema, }) -const rateRevisionWithContractsSchema = rateRevisionSchema.extend({ - contractRevisions: z.array(contractRevisionSchema), -}) - -const contractRevisionWithRatesSchema = contractRevisionSchema.extend({ - rateRevisions: z.array(rateRevisionSchema), -}) - type ContractRevisionType = z.infer type RateRevisionType = z.infer -type RateRevisionWithContractsType = z.infer< - typeof rateRevisionWithContractsSchema -> -type ContractRevisionWithRatesType = z.infer< - typeof contractRevisionWithRatesSchema -> -export { - rateRevisionWithContractsSchema, - contractRevisionWithRatesSchema, - contractRevisionSchema, - rateRevisionSchema, -} +export { contractRevisionSchema, rateRevisionSchema } -export type { - ContractRevisionType, - RateRevisionType, - RateRevisionWithContractsType, - ContractRevisionWithRatesType, -} +export type { ContractRevisionType, RateRevisionType } diff --git a/services/app-api/src/domain-models/index.ts b/services/app-api/src/domain-models/index.ts index 7609f868bf..61381fbef5 100644 --- a/services/app-api/src/domain-models/index.ts +++ b/services/app-api/src/domain-models/index.ts @@ -41,11 +41,9 @@ export { export type { ContractType, ContractRevisionType, - ContractRevisionWithRatesType, ContractFormDataType, RateType, RateRevisionType, - RateRevisionWithContractsType, RateFormDataType, PackageStatusType, ContractPackageSubmissionType, diff --git a/services/app-api/src/emailer/emailer.ts b/services/app-api/src/emailer/emailer.ts index efca99a867..b2aab97ac1 100644 --- a/services/app-api/src/emailer/emailer.ts +++ b/services/app-api/src/emailer/emailer.ts @@ -22,8 +22,9 @@ import type { import type { UpdateInfoType, ProgramType, - ContractRevisionWithRatesType, Question, + ContractRevisionType, + UnlockedContractType, } from '../domain-models' import { SESServiceException } from '@aws-sdk/client-ses' @@ -84,13 +85,13 @@ type Emailer = { statePrograms: ProgramType[] ) => Promise sendUnlockContractCMSEmail: ( - contractRev: ContractRevisionWithRatesType, + contract: UnlockedContractType, updateInfo: UpdateInfoType, stateAnalystsEmails: StateAnalystsEmails, statePrograms: ProgramType[] ) => Promise sendUnlockContractStateEmail: ( - contractRev: ContractRevisionWithRatesType, + contract: UnlockedContractType, updateInfo: UpdateInfoType, statePrograms: ProgramType[], submitterEmails: string[] @@ -120,26 +121,26 @@ type Emailer = { statePrograms: ProgramType[] ) => Promise sendQuestionsStateEmail: ( - contract: ContractRevisionWithRatesType, + contract: ContractRevisionType, submitterEmails: string[], statePrograms: ProgramType[], question: Question ) => Promise sendQuestionsCMSEmail: ( - contract: ContractRevisionWithRatesType, + contract: ContractRevisionType, stateAnalystsEmails: StateAnalystsEmails, statePrograms: ProgramType[], questions: Question[] ) => Promise sendQuestionResponseCMSEmail: ( - contractRevision: ContractRevisionWithRatesType, + contractRevision: ContractRevisionType, statePrograms: ProgramType[], stateAnalystsEmails: StateAnalystsEmails, currentQuestion: Question, allContractQuestions: Question[] ) => Promise sendQuestionResponseStateEmail: ( - contractRevision: ContractRevisionWithRatesType, + contractRevision: ContractRevisionType, statePrograms: ProgramType[], submitterEmails: string[], currentQuestion: Question, diff --git a/services/app-api/src/emailer/emails/__snapshots__/unlockContractCMSEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/unlockContractCMSEmail.test.ts.snap index 6d8e5379ae..1054baeee6 100644 --- a/services/app-api/src/emailer/emails/__snapshots__/unlockContractCMSEmail.test.ts.snap +++ b/services/app-api/src/emailer/emails/__snapshots__/unlockContractCMSEmail.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`unlockPackageCMSEmail renders overall email as expected 1`] = ` -"Submission MCR-MN-0003-SNBC was unlocked
+"Submission MCR-MN-0004-SNBC was unlocked

Unlocked by: leslie@example.com
Unlocked on: 01/01/2022
diff --git a/services/app-api/src/emailer/emails/__snapshots__/unlockContractStateEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/unlockContractStateEmail.test.ts.snap index 53c68417f1..78d6fb3f59 100644 --- a/services/app-api/src/emailer/emails/__snapshots__/unlockContractStateEmail.test.ts.snap +++ b/services/app-api/src/emailer/emails/__snapshots__/unlockContractStateEmail.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders overall email as expected 1`] = ` -"Submission MCR-MN-0003-SNBC was unlocked by CMS
+"Submission MCR-MN-0004-SNBC was unlocked by CMS

Unlocked by: josh@example.com
Unlocked on: 02/01/2022
diff --git a/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts index adcc53c1b5..b271aaa0f4 100644 --- a/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts +++ b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts @@ -10,10 +10,10 @@ import { getQuestionRound, } from '../templateHelpers' import { submissionQuestionResponseURL } from '../generateURLs' -import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' +import type { ContractRevisionType } from '../../domain-models/contractAndRates' export const sendQuestionCMSEmail = async ( - contractRev: ContractRevisionWithRatesType, + contractRev: ContractRevisionType, stateAnalystsEmails: StateAnalystsEmails, config: EmailConfiguration, statePrograms: ProgramType[], diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts index bf09e0a47b..47b74b96d7 100644 --- a/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts +++ b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts @@ -10,11 +10,11 @@ import { getQuestionRound, } from '../templateHelpers' import { submissionQuestionResponseURL } from '../generateURLs' -import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' +import type { ContractRevisionType } from '../../domain-models/contractAndRates' import type { StateAnalystsEmails } from '..' export const sendQuestionResponseCMSEmail = async ( - contractRev: ContractRevisionWithRatesType, + contractRev: ContractRevisionType, config: EmailConfiguration, statePrograms: ProgramType[], stateAnalystsEmails: StateAnalystsEmails, diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts index 04077041c0..11a2279a34 100644 --- a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts @@ -5,7 +5,7 @@ import { } from '../../testHelpers/emailerHelpers' import type { CMSUserType, - ContractRevisionWithRatesType, + ContractRevisionType, StateType, } from '../../domain-models' import type { ContractFormDataType, Question } from '../../domain-models' @@ -126,7 +126,7 @@ test('to addresses list includes submitter emails', async () => { }) test('to addresses list includes all state contacts on submission', async () => { - const sub: ContractRevisionWithRatesType = { + const sub: ContractRevisionType = { ...mockContractRev({ formData }), } const defaultStatePrograms = mockMNState().programs @@ -169,7 +169,7 @@ test('to addresses list does not include duplicate state receiver emails on subm ], } - const sub: ContractRevisionWithRatesType = mockContractRev({ + const sub: ContractRevisionType = mockContractRev({ formData: formDataWithDuplicateStateContacts, }) const defaultStatePrograms = mockMNState().programs diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts index 903e69aab1..d48a28e19e 100644 --- a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts +++ b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts @@ -10,10 +10,10 @@ import { getQuestionRound, } from '../templateHelpers' import { submissionQuestionResponseURL } from '../generateURLs' -import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' +import type { ContractRevisionType } from '../../domain-models/contractAndRates' export const sendQuestionResponseStateEmail = async ( - contractRev: ContractRevisionWithRatesType, + contractRev: ContractRevisionType, config: EmailConfiguration, submitterEmails: string[], statePrograms: ProgramType[], diff --git a/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts index bb7be249dc..a16dbd4301 100644 --- a/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts @@ -5,7 +5,7 @@ import { } from '../../testHelpers/emailerHelpers' import type { CMSUserType, - ContractRevisionWithRatesType, + ContractRevisionType, StateType, } from '../../domain-models' import type { ContractFormDataType, Question } from '../../domain-models' @@ -122,7 +122,7 @@ test('to addresses list includes submitter emails', async () => { }) test('to addresses list includes all state contacts on submission', async () => { - const sub: ContractRevisionWithRatesType = { + const sub: ContractRevisionType = { ...mockContractRev({ formData }), } const defaultStatePrograms = mockMNState().programs @@ -164,7 +164,7 @@ test('to addresses list does not include duplicate state receiver emails on subm ], } - const sub: ContractRevisionWithRatesType = mockContractRev({ + const sub: ContractRevisionType = mockContractRev({ formData: formDataWithDuplicateStateContacts, }) const defaultStatePrograms = mockMNState().programs diff --git a/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts b/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts index 132a42af6d..baf8be30f5 100644 --- a/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts +++ b/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts @@ -9,10 +9,10 @@ import { findContractPrograms, } from '../templateHelpers' import { submissionQuestionResponseURL } from '../generateURLs' -import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' +import type { ContractRevisionType } from '../../domain-models/contractAndRates' export const sendQuestionStateEmail = async ( - contractRev: ContractRevisionWithRatesType, + contractRev: ContractRevisionType, submitterEmails: string[], config: EmailConfiguration, statePrograms: ProgramType[], diff --git a/services/app-api/src/emailer/emails/unlockContractCMSEmail.test.ts b/services/app-api/src/emailer/emails/unlockContractCMSEmail.test.ts index d87c189345..d5c42cadca 100644 --- a/services/app-api/src/emailer/emails/unlockContractCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockContractCMSEmail.test.ts @@ -4,6 +4,7 @@ import { mockMNState, mockMSState, mockContractRev, + mockUnlockedContract, } from '../../testHelpers/emailerHelpers' import { unlockContractCMSEmail } from './index' import { packageName } from '../../common-code/healthPlanFormDataType' @@ -23,12 +24,12 @@ const defaultStatePrograms = mockMNState().programs describe('unlockPackageCMSEmail', () => { test('subject line is correct and clearly states submission is unlocked', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const name = packageName( - sub.contract.stateCode, - sub.contract.stateNumber, - sub.formData.programIDs, + sub.stateCode, + sub.stateNumber, + sub.draftRevision.formData.programIDs, defaultStatePrograms ) const template = await unlockContractCMSEmail( @@ -50,7 +51,8 @@ describe('unlockPackageCMSEmail', () => { ) }) test('includes expected data summary for a contract and rates submission unlock CMS email', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() + const template = await unlockContractCMSEmail( sub, unlockData, @@ -86,15 +88,17 @@ describe('unlockPackageCMSEmail', () => { expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[0].formData.rateCertificationName ?? '' + sub.draftRates?.[0].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) }) test('includes expected data summary for a multi-rate contract and rates submission unlock CMS email', async () => { - const sub = mockContractRev({ - rateRevisions: [ - { + const sub = mockUnlockedContract(undefined, [ + { + id: 'rate-123', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -143,7 +147,10 @@ describe('unlockPackageCMSEmail', () => { ], }, }, - { + }, + { + id: 'rate-234', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -192,7 +199,10 @@ describe('unlockPackageCMSEmail', () => { ], }, }, - { + }, + { + id: 'rate-345', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -241,8 +251,8 @@ describe('unlockPackageCMSEmail', () => { ], }, }, - ], - }) + }, + ]) const template = await unlockContractCMSEmail( sub, unlockData, @@ -280,7 +290,8 @@ describe('unlockPackageCMSEmail', () => { expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[0].formData.rateCertificationName ?? '' + sub.draftRates?.[0].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) @@ -288,7 +299,8 @@ describe('unlockPackageCMSEmail', () => { expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[1].formData.rateCertificationName ?? '' + sub.draftRates?.[1].draftRevision?.formData + .rateCertificationName ?? 'NOPE' ), }) ) @@ -296,14 +308,15 @@ describe('unlockPackageCMSEmail', () => { expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[2].formData.rateCertificationName ?? '' + sub.draftRates?.[2].draftRevision?.formData + .rateCertificationName ?? 'NOPE' ), }) ) }) test('to addresses list includes DMCP and OACT emails for contract and rate package', async () => { - const sub = mockContractRev() - sub.formData.riskBasedContract = true + const sub = mockUnlockedContract() + sub.draftRevision.formData.riskBasedContract = true const template = await unlockContractCMSEmail( sub, unlockData, @@ -343,7 +356,7 @@ describe('unlockPackageCMSEmail', () => { }) test('to addresses list does not include help addresses', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -375,7 +388,7 @@ describe('unlockPackageCMSEmail', () => { }) test('includes state specific analysts emails on contract and rate submission unlock', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -398,8 +411,8 @@ describe('unlockPackageCMSEmail', () => { }) }) test('includes correct toAddresses in contract and rate submission unlock', async () => { - const sub = mockContractRev() - sub.formData.riskBasedContract = true + const sub = mockUnlockedContract() + sub.draftRevision.formData.riskBasedContract = true const template = await unlockContractCMSEmail( sub, unlockData, @@ -441,7 +454,7 @@ describe('unlockPackageCMSEmail', () => { ) }) test('includes state specific analysts emails on contract only submission unlock', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -465,13 +478,20 @@ describe('unlockPackageCMSEmail', () => { }) test('does not include oactEmails on contract only submission unlock', async () => { const mockedContract = mockContractRev() - const sub = mockContractRev({ + const mockedDraftRevision = mockContractRev({ formData: { ...mockedContract.formData, submissionType: 'CONTRACT_ONLY', }, }) + const sub = mockUnlockedContract( + { + draftRevision: mockedDraftRevision, + }, + [] + ) + const template = await unlockContractCMSEmail( sub, unlockData, @@ -494,13 +514,7 @@ describe('unlockPackageCMSEmail', () => { }) }) test('does not include oactEmails on non risked based submission unlock', async () => { - const mockedContract = mockContractRev() - const sub = mockContractRev({ - formData: { - ...mockedContract.formData, - submissionType: 'CONTRACT_AND_RATES', - }, - }) + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -524,7 +538,7 @@ describe('unlockPackageCMSEmail', () => { }) }) test('does not include state analysts emails on contract only submission unlock when none passed in', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -548,18 +562,22 @@ describe('unlockPackageCMSEmail', () => { }) test('CHIP contract only unlock email does include state specific analysts emails', async () => { const mockedContract = mockContractRev() - const sub = mockContractRev({ + const mockedDraftRevision = mockContractRev({ formData: { ...mockedContract.formData, programIDs: ['36c54daf-7611-4a15-8c3b-cdeb3fd7e25a'], submissionType: 'CONTRACT_ONLY', populationCovered: 'CHIP', }, - contract: { - ...mockedContract.contract, + }) + + const sub = mockUnlockedContract( + { stateCode: 'MS', + draftRevision: mockedDraftRevision, }, - }) + [] + ) const msStatePrograms = mockMSState().programs const template = await unlockContractCMSEmail( @@ -584,18 +602,22 @@ describe('unlockPackageCMSEmail', () => { }) test('CHIP contract only unlock email does not include oactEmails or state specific analysts emails', async () => { const mockedContract = mockContractRev() - const sub = mockContractRev({ + const mockedDraftRevision = mockContractRev({ formData: { ...mockedContract.formData, programIDs: ['36c54daf-7611-4a15-8c3b-cdeb3fd7e25a'], submissionType: 'CONTRACT_ONLY', populationCovered: 'CHIP', }, - contract: { - ...mockedContract.contract, + }) + + const sub = mockUnlockedContract( + { stateCode: 'MS', + draftRevision: mockedDraftRevision, }, - }) + [] + ) const msStatePrograms = mockMSState().programs const template = await unlockContractCMSEmail( @@ -628,18 +650,22 @@ describe('unlockPackageCMSEmail', () => { }) test('CHIP contract and rate unlock email does include state specific analysts emails', async () => { const mockedContract = mockContractRev() - const sub = mockContractRev({ + const mockedDraftRevision = mockContractRev({ formData: { ...mockedContract.formData, programIDs: ['36c54daf-7611-4a15-8c3b-cdeb3fd7e25a'], submissionType: 'CONTRACT_ONLY', populationCovered: 'CHIP', }, - contract: { - ...mockedContract.contract, + }) + + const sub = mockUnlockedContract( + { stateCode: 'MS', + draftRevision: mockedDraftRevision, }, - }) + [] + ) const msStatePrograms = mockMSState().programs const template = await unlockContractCMSEmail( @@ -664,18 +690,22 @@ describe('unlockPackageCMSEmail', () => { }) test('CHIP contract and rate unlock email does not include oactEmails or state specific analysts emails', async () => { const mockedContract = mockContractRev() - const sub = mockContractRev({ + const mockedDraftRevision = mockContractRev({ formData: { ...mockedContract.formData, programIDs: ['36c54daf-7611-4a15-8c3b-cdeb3fd7e25a'], submissionType: 'CONTRACT_ONLY', populationCovered: 'CHIP', }, - contract: { - ...mockedContract.contract, + }) + + const sub = mockUnlockedContract( + { stateCode: 'MS', + draftRevision: mockedDraftRevision, }, - }) + [] + ) const msStatePrograms = mockMSState().programs const template = await unlockContractCMSEmail( @@ -707,7 +737,7 @@ describe('unlockPackageCMSEmail', () => { }) }) test('does not include rate name on contract only submission unlock', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, @@ -729,7 +759,7 @@ describe('unlockPackageCMSEmail', () => { }) test('renders overall email as expected', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractCMSEmail( sub, diff --git a/services/app-api/src/emailer/emails/unlockContractCMSEmail.ts b/services/app-api/src/emailer/emails/unlockContractCMSEmail.ts index 1771405307..6645dbb29c 100644 --- a/services/app-api/src/emailer/emails/unlockContractCMSEmail.ts +++ b/services/app-api/src/emailer/emails/unlockContractCMSEmail.ts @@ -1,27 +1,36 @@ import { formatCalendarDate } from '../../../../app-web/src/common-code/dateHelpers' -import type { ContractRevisionWithRatesType } from '../../domain-models' +import type { UnlockedContractType } from '../../domain-models' import { packageName as generatePackageName } from '../../common-code/healthPlanFormDataType' import { stripHTMLFromTemplate, renderTemplate, findContractPrograms, - generateCMSReviewerEmailsForContract, + generateCMSReviewerEmailsForUnlockedContract, } from '../templateHelpers' import type { EmailData, EmailConfiguration, StateAnalystsEmails } from '../' import type { ProgramType, UpdateInfoType } from '../../domain-models' export const unlockContractCMSEmail = async ( - contractRev: ContractRevisionWithRatesType, + contract: UnlockedContractType, updateInfo: UpdateInfoType, config: EmailConfiguration, stateAnalystsEmails: StateAnalystsEmails, statePrograms: ProgramType[] ): Promise => { const isTestEnvironment = config.stage !== 'prod' - const reviewerEmails = generateCMSReviewerEmailsForContract( + const contractRev = contract.draftRevision + const rateRevs = contract.draftRates.map((rate) => { + if (rate.draftRevision) { + return rate.draftRevision + } else { + return rate.packageSubmissions[0].rateRevision + } + }) + + const reviewerEmails = generateCMSReviewerEmailsForUnlockedContract( config, - contractRev, + contract, stateAnalystsEmails ) @@ -36,8 +45,8 @@ export const unlockContractCMSEmail = async ( } const packageName = generatePackageName( - contractRev.contract.stateCode, - contractRev.contract.stateNumber, + contract.stateCode, + contract.stateNumber, contractRev.formData.programIDs, packagePrograms ) @@ -54,7 +63,7 @@ export const unlockContractCMSEmail = async ( contractRev.formData.submissionType === 'CONTRACT_AND_RATES', rateInfos: isContractAndRates && - contractRev.rateRevisions.map((rate) => ({ + rateRevs.map((rate) => ({ rateName: rate.formData.rateCertificationName, })), } diff --git a/services/app-api/src/emailer/emails/unlockContractStateEmail.test.ts b/services/app-api/src/emailer/emails/unlockContractStateEmail.test.ts index a03db2956e..abc5f49095 100644 --- a/services/app-api/src/emailer/emails/unlockContractStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockContractStateEmail.test.ts @@ -1,7 +1,7 @@ import { testEmailConfig, mockMNState, - mockContractRev, + mockUnlockedContract, } from '../../testHelpers/emailerHelpers' import { unlockContractStateEmail } from './index' import { packageName } from '../../common-code/healthPlanFormDataType' @@ -21,12 +21,12 @@ const defaultStatePrograms = mockMNState().programs const defaultSubmitters = ['test1@example.com', 'test2@example.com'] test('subject line is correct and clearly states submission is unlocked', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const name = packageName( - sub.contract.stateCode, - sub.contract.stateNumber, - sub.formData.programIDs, + sub.stateCode, + sub.stateNumber, + sub.draftRevision.formData.programIDs, defaultStatePrograms ) const template = await unlockContractStateEmail( @@ -49,7 +49,7 @@ test('subject line is correct and clearly states submission is unlocked', async }) test('includes expected data summary for a contract and rates submission unlock State email', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractStateEmail( sub, @@ -85,7 +85,8 @@ test('includes expected data summary for a contract and rates submission unlock expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[0].formData.rateCertificationName ?? '' + sub.draftRates?.[0].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) @@ -99,9 +100,10 @@ test('includes expected data summary for a contract and rates submission unlock }) test('includes expected data summary for a multi-rate contract and rates submission unlock State email', async () => { - const sub = mockContractRev({ - rateRevisions: [ - { + const sub = mockUnlockedContract(undefined, [ + { + id: 'rate-123', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -148,7 +150,10 @@ test('includes expected data summary for a multi-rate contract and rates submiss ], }, }, - { + }, + { + id: 'rate-234', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -195,7 +200,10 @@ test('includes expected data summary for a multi-rate contract and rates submiss ], }, }, - { + }, + { + id: 'rate-345', + draftRevision: { id: '12345', rateID: '6789', submitInfo: undefined, @@ -242,8 +250,8 @@ test('includes expected data summary for a multi-rate contract and rates submiss ], }, }, - ], - }) + }, + ]) const template = await unlockContractStateEmail( sub, unlockData, @@ -279,7 +287,8 @@ test('includes expected data summary for a multi-rate contract and rates submiss expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[0].formData.rateCertificationName ?? '' + sub.draftRates?.[0].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) @@ -287,7 +296,8 @@ test('includes expected data summary for a multi-rate contract and rates submiss expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[1].formData.rateCertificationName ?? '' + sub.draftRates?.[1].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) @@ -295,7 +305,8 @@ test('includes expected data summary for a multi-rate contract and rates submiss expect(template).toEqual( expect.objectContaining({ bodyText: expect.stringContaining( - sub.rateRevisions[2].formData.rateCertificationName ?? '' + sub.draftRates?.[2].draftRevision?.formData + .rateCertificationName ?? '' ), }) ) @@ -309,7 +320,7 @@ test('includes expected data summary for a multi-rate contract and rates submiss }) test('does includes the correct submission URL', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractStateEmail( sub, unlockData, @@ -333,7 +344,7 @@ test('does includes the correct submission URL', async () => { }) test('renders overall email as expected', async () => { - const sub = mockContractRev() + const sub = mockUnlockedContract() const template = await unlockContractStateEmail( sub, unlockData, diff --git a/services/app-api/src/emailer/emails/unlockContractStateEmail.ts b/services/app-api/src/emailer/emails/unlockContractStateEmail.ts index c571ec1c24..c66bd6ef11 100644 --- a/services/app-api/src/emailer/emails/unlockContractStateEmail.ts +++ b/services/app-api/src/emailer/emails/unlockContractStateEmail.ts @@ -6,12 +6,15 @@ import { findContractPrograms, } from '../templateHelpers' import type { EmailData, EmailConfiguration } from '../' -import type { ProgramType, UpdateInfoType } from '../../domain-models' +import type { + ProgramType, + UnlockedContractType, + UpdateInfoType, +} from '../../domain-models' import { reviewAndSubmitURL } from '../generateURLs' import { pruneDuplicateEmails } from '../formatters' -import type { ContractRevisionWithRatesType } from '../../domain-models' export const unlockContractStateEmail = async ( - contractRev: ContractRevisionWithRatesType, + contract: UnlockedContractType, updateInfo: UpdateInfoType, config: EmailConfiguration, statePrograms: ProgramType[], @@ -19,6 +22,16 @@ export const unlockContractStateEmail = async ( ): Promise => { const isTestEnvironment = config.stage !== 'prod' const stateContactEmails: string[] = [] + + const contractRev = contract.draftRevision + const rateRevs = contract.draftRates.map((rate) => { + if (rate.draftRevision) { + return rate.draftRevision + } else { + return rate.packageSubmissions[0].rateRevision + } + }) + const contractFormData = contractRev.formData contractFormData.stateContacts.forEach((contact) => { if (contact.email) stateContactEmails.push(contact.email) @@ -37,15 +50,15 @@ export const unlockContractStateEmail = async ( } const packageName = generatePackageName( - contractRev.contract.stateCode, - contractRev.contract.stateNumber, + contract.stateCode, + contract.stateNumber, contractRev.formData.programIDs, packagePrograms ) const isContractAndRates = contractFormData.submissionType === 'CONTRACT_AND_RATES' && - Boolean(contractRev.rateRevisions.length) + Boolean(rateRevs.length) const contractURL = reviewAndSubmitURL( contractRev.contract.id, @@ -61,7 +74,7 @@ export const unlockContractStateEmail = async ( contractFormData.submissionType === 'CONTRACT_AND_RATES', rateInfos: isContractAndRates && - contractRev.rateRevisions.map((rate) => ({ + rateRevs.map((rate) => ({ rateName: rate.formData.rateCertificationName, })), submissionURL: contractURL, diff --git a/services/app-api/src/emailer/templateHelpers.ts b/services/app-api/src/emailer/templateHelpers.ts index c2da0f2e3b..eff4eeca46 100644 --- a/services/app-api/src/emailer/templateHelpers.ts +++ b/services/app-api/src/emailer/templateHelpers.ts @@ -8,8 +8,10 @@ import type { } from '../../../app-web/src/common-code/healthPlanFormDataType' import type { EmailConfiguration, StateAnalystsEmails } from '.' import type { - ContractRevisionWithRatesType, + ContractRevisionType, ProgramType, + RateRevisionType, + UnlockedContractType, } from '../domain-models' import { logError } from '../logger' import { pruneDuplicateEmails } from './formatters' @@ -80,7 +82,8 @@ const handleAsCHIPSubmission = ( } const handleAsCHIPSubmissionForContract = ( - contractRev: ContractRevisionWithRatesType + contractRev: ContractRevisionType, + rateRevs: RateRevisionType[] ): boolean => { const contractFormData = contractRev.formData // This const is deprecated. No longer in use once we added population covered question, code remains only for backwards compatibility for existing Mississippi submissions. @@ -94,7 +97,7 @@ const handleAsCHIPSubmissionForContract = ( !contractFormData.populationCovered && contractRev.contract.stateCode === 'MS' ) { - const programIDs = findAllContractProgramIds(contractRev) + const programIDs = findAllContractProgramIds(contractRev, rateRevs) return programIDs.some( (id: string) => LEGACY_CHIP_PROGRAMS_UUID.MS === id ) @@ -125,11 +128,12 @@ const filterChipAndPRSubmissionReviewers = ( Return should be wrapped in pruneDuplicate to ensure even if config is added twice, we get unique list of reviewers */ -const generateCMSReviewerEmailsForContract = ( +const generateCMSReviewerEmailsForUnlockedContract = ( config: EmailConfiguration, - contractRev: ContractRevisionWithRatesType, + contract: UnlockedContractType, stateAnalystsEmails: StateAnalystsEmails ): string[] | Error => { + const contractRev = contract.draftRevision const contractFormData = contractRev.formData if ( contractFormData.submissionType !== 'CONTRACT_AND_RATES' && @@ -163,9 +167,12 @@ const generateCMSReviewerEmailsForContract = ( } } + const rateRevs = contract.draftRates.map( + (rate) => rate.draftRevision || rate.packageSubmissions[0].rateRevision + ) //Remove OACT and DMCP emails from CHIP or State of PR submissions if ( - handleAsCHIPSubmissionForContract(contractRev) || + handleAsCHIPSubmissionForContract(contractRev, rateRevs) || contractRev.contract.stateCode === 'PR' ) { reviewers = filterChipAndPRSubmissionReviewers(reviewers, config) @@ -247,15 +254,16 @@ const findAllPackageProgramIds = ( //Finds all contract program and rate program ids in a contract and combines them into one array removing duplicates. const findAllContractProgramIds = ( - contractRev: ContractRevisionWithRatesType + contractRev: ContractRevisionType, + rateRevs: RateRevisionType[] ): string[] => { const contractFormData = contractRev.formData const programs = [...contractFormData.programIDs] if ( contractFormData.submissionType === 'CONTRACT_AND_RATES' && - !!contractRev.rateRevisions?.length + !!rateRevs?.length ) { - const ratePrograms = contractRev.rateRevisions?.flatMap( + const ratePrograms = rateRevs?.flatMap( (rateInfo) => rateInfo.formData.rateProgramIDs ) if (ratePrograms?.length) { @@ -286,7 +294,7 @@ const findPackagePrograms = ( //Find state programs from contract with rates const findContractPrograms = ( - contractRev: ContractRevisionWithRatesType, + contractRev: ContractRevisionType, statePrograms: ProgramType[] ): ProgramType[] | Error => { const programIDs = contractRev.formData.programIDs @@ -352,7 +360,7 @@ export { handleAsCHIPSubmission, handleAsCHIPSubmissionForContract, generateCMSReviewerEmails, - generateCMSReviewerEmailsForContract, + generateCMSReviewerEmailsForUnlockedContract, renderTemplate, SubmissionTypeRecord, findAllPackageProgramIds, diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts index 352588f8e2..3b2338a8d1 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts @@ -15,9 +15,7 @@ import { mockInsertRateArgs } from '../../testHelpers/rateDataMocks' import { convertContractToDraftRateRevisions } from '../../domain-models/contractAndRates/convertContractWithRatesToHPP' import { updateDraftContractRates } from './updateDraftContractRates' -// TODO: Enable these tests again after reimplementing rate change history that was in contractWithHistoryToDomainModel -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('findContractWithHistory with full contract and rate history', () => { +describe('findContractWithHistory with full contract and rate history', () => { it('finds a stripped down contract with history', async () => { const client = await sharedTestPrismaClient() @@ -49,13 +47,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () const contractA = must( await insertDraftContract(client, draftContractData) ) - must( - await submitContract(client, { - contractID: contractA.id, - submittedByUserID: stateUser.id, - submittedReason: 'initial submit', - }) - ) // Add 3 rates 1, 2, 3 pointing to contract A const rate1 = must( @@ -71,13 +62,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () contractIDs: [contractA.id], }) ) - must( - await submitRate(client, { - rateID: rate1.id, - submittedByUserID: stateUser.id, - submittedReason: 'Rate Submit', - }) - ) const rate2 = must( await insertDraftRate(client, contractA.id, { @@ -92,13 +76,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () contractIDs: [contractA.id], }) ) - must( - await submitRate(client, { - rateID: rate2.id, - submittedByUserID: stateUser.id, - submittedReason: 'RateSubmit 2', - }) - ) const rate3 = must( await insertDraftRate(client, contractA.id, { @@ -113,11 +90,12 @@ describe.skip('findContractWithHistory with full contract and rate history', () contractIDs: [contractA.id], }) ) + must( - await submitRate(client, { - rateID: rate3.id, + await submitContract(client, { + contractID: contractA.id, submittedByUserID: stateUser.id, - submittedReason: '3.0 create', + submittedReason: 'initial submit', }) ) @@ -128,7 +106,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () if (threeContract instanceof Error) { throw threeContract } - expect(threeContract.revisions).toHaveLength(4) + expect(threeContract.packageSubmissions).toHaveLength(1) // remove the connection from rate 2 must( @@ -160,8 +138,8 @@ describe.skip('findContractWithHistory with full contract and rate history', () if (twoContract instanceof Error) { throw twoContract } - expect(twoContract.revisions).toHaveLength(5) - expect(twoContract.revisions[0].rateRevisions).toHaveLength(2) + expect(twoContract.packageSubmissions).toHaveLength(2) + expect(twoContract.packageSubmissions[0].rateRevisions).toHaveLength(2) // update rate 1 to have a new version, should make one new rev. must( @@ -193,7 +171,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () if (backAgainContract instanceof Error) { throw backAgainContract } - expect(backAgainContract.revisions).toHaveLength(6) + expect(backAgainContract.packageSubmissions).toHaveLength(3) // Make a new Contract Revision, should show up as a single new rev with all the old info must( @@ -218,7 +196,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () if (testingContract instanceof Error) { throw testingContract } - expect(testingContract.revisions).toHaveLength(7) + expect(testingContract.packageSubmissions).toHaveLength(4) // Make a new Contract Revision, changing the connections should show up as a single new rev. const unlockedContractA = must( @@ -289,7 +267,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () if (testingContract instanceof Error) { throw testingContract } - expect(testingContract.revisions).toHaveLength(8) + expect(testingContract.packageSubmissions).toHaveLength(5) // Now, find that contract and assert the history is what we expected const resultingContract = must( @@ -299,91 +277,80 @@ describe.skip('findContractWithHistory with full contract and rate history', () throw resultingContract } - const revisionsInTimeOrder = resultingContract.revisions.reverse() - - console.info( - 'ALL First REvisions: ', - JSON.stringify(revisionsInTimeOrder, null, ' ') - ) + const submissionsInTimeOrder = + resultingContract.packageSubmissions.reverse() - // Each Revision needs a Reason, one of the contracts or revisions associated with it should have changed and why. + // console.info( + // 'ALL First REvisions: ', + // JSON.stringify(submissionsInTimeOrder, null, ' ') + // ) - expect(revisionsInTimeOrder).toHaveLength(8) - expect(revisionsInTimeOrder[0].rateRevisions).toHaveLength(0) - expect(revisionsInTimeOrder[0].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[0].submitInfo?.updatedReason).toBe( + // Each submission needs a Reason, one of the contracts or revisions associated with it should have changed and why. + expect(submissionsInTimeOrder).toHaveLength(5) + expect( + submissionsInTimeOrder[0].contractRevision.unlockInfo + ).toBeUndefined() + expect(submissionsInTimeOrder[0].rateRevisions).toHaveLength(3) + expect(submissionsInTimeOrder[0].submitInfo?.updatedReason).toBe( 'initial submit' ) - expect(revisionsInTimeOrder[1].rateRevisions).toHaveLength(1) - expect(revisionsInTimeOrder[1].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[1].submitInfo?.updatedReason).toBe( - 'Rate Submit' - ) - - expect(revisionsInTimeOrder[2].rateRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[2].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[2].submitInfo?.updatedReason).toBe( - 'RateSubmit 2' - ) - - expect(revisionsInTimeOrder[3].rateRevisions).toHaveLength(3) - expect(revisionsInTimeOrder[3].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[3].submitInfo?.updatedReason).toBe( - '3.0 create' - ) - - expect(revisionsInTimeOrder[4].rateRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[4].unlockInfo?.updatedReason).toBe( - 'unlock for 2.1 remove' - ) - expect(revisionsInTimeOrder[4].unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect(revisionsInTimeOrder[4].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[1].rateRevisions).toHaveLength(2) + expect( + submissionsInTimeOrder[1].contractRevision.unlockInfo + ).toBeUndefined() + expect( + submissionsInTimeOrder[1].submittedRevisions[0].unlockInfo + ?.updatedReason + ).toBe('unlock for 2.1 remove') + expect( + submissionsInTimeOrder[1].submittedRevisions[0].unlockInfo + ?.updatedBy.email + ).toBe('zuko@example.com') + expect(submissionsInTimeOrder[1].submitInfo?.updatedReason).toBe( '2.1 remove' ) - expect(revisionsInTimeOrder[5].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[2].rateRevisions).toHaveLength(2) expect( - revisionsInTimeOrder[5].rateRevisions[1].formData + submissionsInTimeOrder[2].rateRevisions[0].formData .rateCertificationName ).toBe('onepointone') - expect(revisionsInTimeOrder[5].unlockInfo?.updatedReason).toBe( - 'unlock for 1.1' - ) - expect(revisionsInTimeOrder[5].submitInfo?.updatedReason).toBe( + expect( + submissionsInTimeOrder[2].rateRevisions[0].unlockInfo?.updatedReason + ).toBe('unlock for 1.1') + expect(submissionsInTimeOrder[2].submitInfo?.updatedReason).toBe( '1.1 new name' ) - expect(revisionsInTimeOrder[6].rateRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[6].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[3].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[3].submitInfo?.updatedReason).toBe( 'Submitting A.1' ) - expect(revisionsInTimeOrder[7].rateRevisions).toHaveLength(1) - expect(revisionsInTimeOrder[7].formData).toEqual( + expect(submissionsInTimeOrder[4].rateRevisions).toHaveLength(1) + expect(submissionsInTimeOrder[4].contractRevision.formData).toEqual( expect.objectContaining({ submissionType: 'CONTRACT_AND_RATES', submissionDescription: 'a.2 body', }) ) - expect(revisionsInTimeOrder[7].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[4].submitInfo?.updatedReason).toBe( 'Submitting A.2' ) // check for rate and see if it handles the removed bit right - const rate1fetched = await findRateWithHistory(client, rate1.id) if (rate1fetched instanceof Error) { throw rate1fetched } - expect(rate1fetched.revisions).toHaveLength(4) - expect(rate1fetched.revisions[0].submitInfo?.updatedReason).toBe( - 'Submitting A.2' - ) + expect(rate1fetched.packageSubmissions).toHaveLength(4) + expect( + rate1fetched.packageSubmissions[0].submitInfo?.updatedReason + ).toBe('Submitting A.2') }) + it('finds a full contract', async () => { const client = await sharedTestPrismaClient() @@ -415,13 +382,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () const contractA = must( await insertDraftContract(client, draftContractData) ) - must( - await submitContract(client, { - contractID: contractA.id, - submittedByUserID: stateUser.id, - submittedReason: 'initial submit', - }) - ) // Add 3 rates 1, 2, 3 pointing to contract A const rate1 = must( @@ -441,13 +401,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () } ) ) - must( - await submitRate(client, { - rateID: rate1.id, - submittedByUserID: stateUser.id, - submittedReason: 'Rate Submit', - }) - ) const rate2 = must( await insertDraftRate(client, contractA.id, { @@ -462,13 +415,6 @@ describe.skip('findContractWithHistory with full contract and rate history', () contractIDs: [contractA.id], }) ) - must( - await submitRate(client, { - rateID: rate2.id, - submittedByUserID: stateUser.id, - submittedReason: 'RateSubmit 2', - }) - ) const rate3 = must( await insertDraftRate(client, contractA.id, { @@ -483,11 +429,12 @@ describe.skip('findContractWithHistory with full contract and rate history', () contractIDs: [contractA.id], }) ) + must( - await submitRate(client, { - rateID: rate3.id, + await submitContract(client, { + contractID: contractA.id, submittedByUserID: stateUser.id, - submittedReason: '3.0 create', + submittedReason: 'initial submit', }) ) @@ -625,57 +572,58 @@ describe.skip('findContractWithHistory with full contract and rate history', () throw resultingContract } - const revisions = resultingContract.revisions.reverse() + const submissionsInTimeOrder = + resultingContract.packageSubmissions.reverse() - console.info( - 'ALL First REvisions: ', - JSON.stringify(revisions, null, ' ') - ) + // console.info( + // 'ALL First REvisions: ', + // JSON.stringify(submissionsInTimeOrder, null, ' ') + // ) // Each Revision needs a Reason, one of the contracts or revisions associated with it should have changed and why. + expect(submissionsInTimeOrder).toHaveLength(5) + expect(submissionsInTimeOrder[0].rateRevisions).toHaveLength(3) + expect(submissionsInTimeOrder[0].submitInfo?.updatedReason).toBe( + 'initial submit' + ) - expect(revisions).toHaveLength(8) - expect(revisions[0].rateRevisions).toHaveLength(0) - expect(revisions[0].submitInfo?.updatedReason).toBe('initial submit') - - expect(revisions[1].rateRevisions).toHaveLength(1) - expect(revisions[1].submitInfo?.updatedReason).toBe('Rate Submit') - - expect(revisions[2].rateRevisions).toHaveLength(2) - expect(revisions[2].submitInfo?.updatedReason).toBe('RateSubmit 2') - - expect(revisions[3].rateRevisions).toHaveLength(3) - expect(revisions[4].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[1].rateRevisions).toHaveLength(2) - expect(revisions[5].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[2].rateRevisions).toHaveLength(2) expect( - revisions[5].rateRevisions[1].formData.rateCertificationName + submissionsInTimeOrder[2].rateRevisions[0].formData + .rateCertificationName ).toBe('onepointone') - expect(revisions[5].submitInfo?.updatedReason).toBe('1.1 new name') + expect(submissionsInTimeOrder[2].submitInfo?.updatedReason).toBe( + '1.1 new name' + ) - expect(revisions[6].rateRevisions).toHaveLength(2) - expect(revisions[6].submitInfo?.updatedReason).toBe('Submitting A.1') + expect(submissionsInTimeOrder[3].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[3].submitInfo?.updatedReason).toBe( + 'Submitting A.1' + ) - expect(revisions[7].rateRevisions).toHaveLength(1) - expect(revisions[7].formData).toEqual( + expect(submissionsInTimeOrder[4].rateRevisions).toHaveLength(1) + expect(submissionsInTimeOrder[4].contractRevision.formData).toEqual( expect.objectContaining({ submissionType: 'CONTRACT_AND_RATES', submissionDescription: 'a.2 body', }) ) - expect(revisions[7].submitInfo?.updatedReason).toBe('Submitting A.2') + expect(submissionsInTimeOrder[4].submitInfo?.updatedReason).toBe( + 'Submitting A.2' + ) // check for rate and see if it handles the removed bit right - const rate1fetched = await findRateWithHistory(client, rate1.id) if (rate1fetched instanceof Error) { throw rate1fetched } - expect(rate1fetched.revisions).toHaveLength(4) - expect(rate1fetched.revisions[0].submitInfo?.updatedReason).toBe( - 'Submitting A.2' - ) + expect(rate1fetched.packageSubmissions).toHaveLength(4) + expect( + rate1fetched.packageSubmissions[0].submitInfo?.updatedReason + ).toBe('Submitting A.2') }) it('handles drafts correctly', async () => { const client = await sharedTestPrismaClient() @@ -712,6 +660,11 @@ describe.skip('findContractWithHistory with full contract and rate history', () stateCode: 'MN', rateCertificationName: 'twopoint0', }) + const rate3 = mockInsertRateArgs({ + id: uuidv4(), + stateCode: 'MN', + rateCertificationName: 'threepoint0', + }) // add a contract that has both of them. const draftContractData = mockInsertContractArgs({ @@ -720,7 +673,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () const contractA = must( await insertDraftContract(client, draftContractData) ) - const updatedDraftContractWithRates = must( + must( await updateDraftContractFormData(client, { contractID: contractA.id, formData: { @@ -734,7 +687,7 @@ describe.skip('findContractWithHistory with full contract and rate history', () }) ) - must( + const updatedDraftContractAWithRates = must( await updateDraftContractRates(client, { contractID: contractA.id, rateUpdates: { @@ -756,27 +709,19 @@ describe.skip('findContractWithHistory with full contract and rate history', () }) ) - must( - await submitContract(client, { - contractID: contractA.id, - submittedByUserID: stateUser.id, - submittedReason: 'Submitting A.2', - }) - ) - - if (!updatedDraftContractWithRates.draftRevision) { + if (!updatedDraftContractAWithRates.draftRevision) { throw new Error( 'Unexpected error: draftRevision does not exist in contract' ) } - if (!updatedDraftContractWithRates.draftRates) { + if (!updatedDraftContractAWithRates.draftRates) { throw new Error('Unexpected error: revisions missing draftRates') } const draftRateRevisionData1 = - updatedDraftContractWithRates.draftRates[0].draftRevision?.formData + updatedDraftContractAWithRates.draftRates[0].draftRevision?.formData const draftRateRevisionData2 = - updatedDraftContractWithRates.draftRates[1].draftRevision?.formData + updatedDraftContractAWithRates.draftRates[1].draftRevision?.formData if ( !draftRateRevisionData1?.rateID || @@ -785,36 +730,61 @@ describe.skip('findContractWithHistory with full contract and rate history', () throw new Error('Unexpected error: rate revision is missing rateID') } - // submit both rates + // submit contract must( - await submitRate(client, { - rateID: draftRateRevisionData1.rateID, + await submitContract(client, { + contractID: contractA.id, submittedByUserID: stateUser.id, - submittedReason: 'Rate Submit', + submittedReason: 'initial submit', }) ) - const submittedRate2 = must( - await submitRate(client, { - rateID: draftRateRevisionData2.rateID, - submittedByUserID: stateUser.id, - submittedReason: 'Rate Submit 2', + const rate1ID = updatedDraftContractAWithRates.draftRates[0].id + + // add a contract that has both of them. + const draftContractBData = mockInsertContractArgs({ + submissionDescription: 'two contract', + }) + const contractB = must( + await insertDraftContract(client, draftContractBData) + ) + + must( + await updateDraftContractRates(client, { + contractID: contractB.id, + rateUpdates: { + create: [ + { + formData: rate3, + ratePosition: 1, + }, + ], + update: [], + link: [ + { + rateID: rate1ID, + ratePosition: 2, + }, + ], + unlink: [], + delete: [], + }, }) ) // submit contract must( await submitContract(client, { - contractID: contractA.id, + contractID: contractB.id, submittedByUserID: stateUser.id, submittedReason: 'initial submit', }) ) - // Unlock contract A, but don't resubmit it yet. + // Unlock contract B, but don't resubmit it yet. must( await unlockContract(client, { - contractID: contractA.id, + contractID: contractB.id, unlockedByUserID: cmsUser.id, unlockReason: 'unlock A Open', }) @@ -822,47 +792,54 @@ describe.skip('findContractWithHistory with full contract and rate history', () // Draft should pull revision 2.0 out const draftPreRateUnlock = must( - await findContractWithHistory(client, contractA.id) + await findContractWithHistory(client, contractB.id) ) expect(draftPreRateUnlock.draftRevision).toBeDefined() expect( draftPreRateUnlock.draftRates && draftPreRateUnlock.draftRates.map( - (rr) => rr?.draftRevision?.formData.rateCertificationName + (rr) => + rr?.draftRevision?.formData.rateCertificationName || + rr.packageSubmissions[0].rateRevision.formData + .rateCertificationName ) - ).toEqual(['onepoint0', 'twopoint0']) + ).toEqual(['threepoint0', 'onepoint0']) - // unlock and submit second rate rev + // unlock and submit second rate one must( await unlockRate(client, { - rateID: submittedRate2.id, + rateID: rate1ID, unlockedByUserID: cmsUser.id, unlockReason: 'unlock for 2.1', }) ) must( await updateDraftRate(client, { - rateID: submittedRate2.id, - formData: { rateCertificationName: 'twopointone' }, - contractIDs: [contractA.id], + rateID: rate1ID, + formData: { rateCertificationName: 'onepointone' }, + contractIDs: [contractA.id, contractB.id], }) ) // Draft should now pull draft revision 2.1 out, even though its unsubmitted const draftPreRateSubmit = must( - await findContractWithHistory(client, contractA.id) + await findContractWithHistory(client, contractB.id) ) expect(draftPreRateSubmit.draftRevision).toBeDefined() expect( - convertContractToDraftRateRevisions(draftPreRateSubmit).map( - (rr) => rr.formData.rateCertificationName - ) - ).toEqual(['onepoint0', 'twopointone']) + draftPreRateSubmit.draftRates && + draftPreRateSubmit.draftRates.map( + (rr) => + rr?.draftRevision?.formData.rateCertificationName || + rr.packageSubmissions[0].rateRevision.formData + .rateCertificationName + ) + ).toEqual(['threepoint0', 'onepointone']) // Submit Rate 2.1 must( await submitRate(client, { - rateID: submittedRate2.id, + rateID: rate1ID, submittedByUserID: stateUser.id, submittedReason: '2.1 update', }) @@ -870,67 +847,72 @@ describe.skip('findContractWithHistory with full contract and rate history', () // raft should still pull revision 2.1 out const draftPostRateSubmit = must( - await findContractWithHistory(client, contractA.id) + await findContractWithHistory(client, contractB.id) ) expect(draftPostRateSubmit.draftRevision).toBeDefined() expect( - convertContractToDraftRateRevisions(draftPostRateSubmit).map( - (rr) => rr.formData.rateCertificationName - ) - ).toEqual(['onepoint0', 'twopointone']) + draftPreRateSubmit.draftRates && + draftPreRateSubmit.draftRates.map( + (rr) => + rr?.draftRevision?.formData.rateCertificationName || + rr.packageSubmissions[0].rateRevision.formData + .rateCertificationName + ) + ).toEqual(['threepoint0', 'onepointone']) - // submit contract A1, now, should show up as a single new rev and have the latest rates + // submit contract B1, now, should show up as a single new rev and have the latest rates must( await submitContract(client, { - contractID: contractA.id, + contractID: contractB.id, submittedByUserID: stateUser.id, submittedReason: 'third submit', }) ) // attempt a second submission, should result in an error. - const contractA_1_Error = await submitContract(client, { - contractID: contractA.id, + const contractB_1_Error = await submitContract(client, { + contractID: contractB.id, submittedByUserID: stateUser.id, submittedReason: 'third submit', }) - if (!(contractA_1_Error instanceof Error)) { + if (!(contractB_1_Error instanceof Error)) { throw new Error('Should be impossible to submit twice in a row.') } - const res = must(await findContractWithHistory(client, contractA.id)) + const res = must(await findContractWithHistory(client, contractB.id)) - const revisions = res.revisions.reverse() + const submissionsInTimeOrder = res.packageSubmissions.reverse() - console.info( - 'ALL First REvisions: ', - JSON.stringify(revisions, null, ' ') + expect(submissionsInTimeOrder).toHaveLength(3) + expect(submissionsInTimeOrder[0].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[0].submitInfo?.updatedReason).toBe( + 'initial submit' ) - expect(revisions).toHaveLength(3) - expect(revisions[0].rateRevisions).toHaveLength(2) - expect(revisions[0].submitInfo?.updatedReason).toBe('initial submit') - - expect(revisions[1].rateRevisions).toHaveLength(2) - expect(revisions[1].submitInfo?.updatedReason).toBe('2.1 update') + expect(submissionsInTimeOrder[1].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[1].submitInfo?.updatedReason).toBe( + '2.1 update' + ) - expect(revisions[2].rateRevisions).toHaveLength(2) - expect(revisions[2].submitInfo?.updatedReason).toBe('third submit') + expect(submissionsInTimeOrder[2].rateRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[2].submitInfo?.updatedReason).toBe( + 'third submit' + ) // these revisions can be in any order because they were saved at the same time const revisionFormDatas = new Set( - revisions[2].rateRevisions.map( + submissionsInTimeOrder[2].rateRevisions.map( (rr) => rr.formData.rateCertificationName ) ) - const expectedFormDatas = new Set(['onepoint0', 'twopointone']) + const expectedFormDatas = new Set(['threepoint0', 'onepointone']) expect(revisionFormDatas).toStrictEqual(expectedFormDatas) }) }) describe('findContractWithHistory with only contract history', () => { // eslint-disable-next-line jest/no-disabled-tests - it.skip('matches correct rate revisions to contract revision with independent rate unlocks and submits', async () => { + it('matches correct rate revisions to contract revision with independent rate unlocks and submits', async () => { const client = await sharedTestPrismaClient() const stateUser = await client.user.create({ @@ -1053,7 +1035,7 @@ describe('findContractWithHistory with only contract history', () => { // Expect rate revision on contract revision to be rate revision 1.3 expect( - submittedContract.revisions[0].rateRevisions[0].submitInfo + submittedContract.packageSubmissions[0].rateRevisions[0].submitInfo ?.updatedReason ).toBe('submit rate A revision 1.3') @@ -1106,7 +1088,7 @@ describe('findContractWithHistory with only contract history', () => { // Expect latest contract revisions matching rate revision to be version 1.5 expect( - submittedContract.revisions[0].rateRevisions[0].submitInfo + submittedContract.packageSubmissions[0].rateRevisions[0].submitInfo ?.updatedReason ).toBe('submit rate A revision 1.5') @@ -1115,12 +1097,6 @@ describe('findContractWithHistory with only contract history', () => { 'submit contract revision 1.0' ) - // Expect latest contract revisions matching rate revision to be version 1.3 - expect( - submittedContract.revisions[1].rateRevisions[0].submitInfo - ?.updatedReason - ).toBe('submit rate A revision 1.3') - // Add a second rate to contract and submit const unlockedContract = must( await unlockContract(client, { @@ -1227,11 +1203,11 @@ describe('findContractWithHistory with only contract history', () => { 'submit contract revision 1.2' ) expect( - submittedContract.revisions[0].rateRevisions[0].submitInfo + submittedContract.packageSubmissions[0].rateRevisions[0].submitInfo ?.updatedReason ).toBe('submit contract revision 1.2') expect( - submittedContract.revisions[0].rateRevisions[1].submitInfo + submittedContract.packageSubmissions[0].rateRevisions[1].submitInfo ?.updatedReason ).toBe('submit rate B revision 1.2') @@ -1239,16 +1215,9 @@ describe('findContractWithHistory with only contract history', () => { expect(submittedContract.revisions[1].submitInfo?.updatedReason).toBe( 'submit contract revision 1.1' ) - expect( - submittedContract.revisions[1].rateRevisions[0].submitInfo - ?.updatedReason - ).toBe('submit rate A revision 1.5') + expect(submittedContract.revisions[2].submitInfo?.updatedReason).toBe( 'submit contract revision 1.0' ) - expect( - submittedContract.revisions[2].rateRevisions[0].submitInfo - ?.updatedReason - ).toBe('submit rate A revision 1.3') }) }) diff --git a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts index 17fbcc61a2..dbb6bdece1 100644 --- a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.test.ts @@ -17,9 +17,7 @@ import { updateDraftContractRates } from './updateDraftContractRates' import { convertContractToDraftRateRevisions } from '../../domain-models/contractAndRates/convertContractWithRatesToHPP' describe('findRate', () => { - // TODO: Enable this tests again after reimplementing rate change history that was in contractWithHistoryToDomainModel - // eslint-disable-next-line jest/no-disabled-tests - it.skip('finds a stripped down rate with history', async () => { + it('finds a stripped down rate with history', async () => { const client = await sharedTestPrismaClient() const stateUser = await client.user.create({ @@ -49,13 +47,6 @@ describe('findRate', () => { const contractA = must( await insertDraftContract(client, draftContractData) ) - must( - await submitContract(client, { - contractID: contractA.id, - submittedByUserID: stateUser.id, - submittedReason: 'Submitting A.1', - }) - ) // setup a single test rate const draftRateData = mockInsertRateArgs({ @@ -71,6 +62,14 @@ describe('findRate', () => { ) } + must( + await submitContract(client, { + contractID: contractA.id, + submittedByUserID: stateUser.id, + submittedReason: 'First Contract Submit', + }) + ) + // Add 3 contracts 1, 2, 3 pointing to rate A const contract1 = must( await insertDraftContract(client, { @@ -175,13 +174,13 @@ describe('findRate', () => { ) // Submit rateA - const submittedRateA = must( - await submitRate(client, { - rateID: rateA.id, - submittedByUserID: stateUser.id, - submittedReason: 'initial rate submit', - }) - ) + // const submittedRateA = must( + // await submitRate(client, { + // rateID: rateA.id, + // submittedByUserID: stateUser.id, + // submittedReason: 'initial rate submit', + // }) + // ) // Submit Contract 1, 2, and 3 must( @@ -211,7 +210,10 @@ describe('findRate', () => { if (threeRate instanceof Error) { throw threeRate } - expect(threeRate.revisions).toHaveLength(4) + expect(threeRate.packageSubmissions).toHaveLength(4) + expect(threeRate.packageSubmissions[0].contractRevisions).toHaveLength( + 4 + ) // remove the connection from contract 2 const unlockedContract2 = must( @@ -236,14 +238,14 @@ describe('findRate', () => { must( await updateDraftContractRates(client, { - contractID: contract3.id, + contractID: contract2.id, rateUpdates: { create: [], update: [], link: [], unlink: [ { - rateID: submittedRateA.id, + rateID: rateA.id, }, ], delete: [], @@ -260,14 +262,13 @@ describe('findRate', () => { ) // Now, find that rate and assert the history is what we expected - const twoRate = must( - await findRateWithHistory(client, submittedRateA.id) - ) + const twoRate = must(await findRateWithHistory(client, rateA.id)) if (twoRate instanceof Error) { throw twoRate } - expect(twoRate.revisions).toHaveLength(5) - expect(twoRate.revisions[0].contractRevisions).toHaveLength(2) + expect(twoRate.packageSubmissions).toHaveLength(5) + + expect(twoRate.packageSubmissions[0].contractRevisions).toHaveLength(3) // update contract 1 to have a new version, should make one new rev. const unlockedContract1 = must( @@ -292,15 +293,13 @@ describe('findRate', () => { ) // Now, find that rate and assert the history is what we expected - const backAgainRate = must( - await findRateWithHistory(client, submittedRateA.id) - ) + const backAgainRate = must(await findRateWithHistory(client, rateA.id)) if (backAgainRate instanceof Error) { throw backAgainRate } - expect(backAgainRate.revisions).toHaveLength(6) + expect(backAgainRate.packageSubmissions).toHaveLength(6) - // Make a new Contract Revision, should show up as a single new rev with all the old info + // Make a new Rate Revision, should show up as a single new rev with all the old info must( await unlockRate(client, { rateID: rateA.id, @@ -317,13 +316,14 @@ describe('findRate', () => { ) // Now, find that contract and assert the history is what we expected - let testingRate = must( + const testingRate = must( await findRateWithHistory(client, resubmittedRateA.id) ) if (testingRate instanceof Error) { throw testingRate } - expect(testingRate.revisions).toHaveLength(7) + expect(testingRate.revisions).toHaveLength(2) + expect(testingRate.packageSubmissions).toHaveLength(7) // Make a new Rate Revision, changing the connections should show up as a single new rev. const secondUnlockRateA = must( @@ -337,6 +337,7 @@ describe('findRate', () => { await updateDraftRate(client, { rateID: secondUnlockRateA.id, formData: { + rateCertificationName: 'one contract', rateType: 'AMENDMENT', }, contractIDs: [contract3.id], @@ -351,13 +352,13 @@ describe('findRate', () => { ) // Now, find that contract and assert the history is what we expected - testingRate = must( + const testingRateTwo = must( await findRateWithHistory(client, secondResubmitRateA.id) ) - if (testingRate instanceof Error) { - throw testingRate + if (testingRateTwo instanceof Error) { + throw testingRateTwo } - expect(testingRate.revisions).toHaveLength(8) + expect(testingRateTwo.packageSubmissions).toHaveLength(8) // Now, find that contract and assert the history is what we expected const resultingRate = must( @@ -367,70 +368,70 @@ describe('findRate', () => { throw resultingRate } - const revisionsInTimeOrder = resultingRate.revisions.reverse() + const submissionsInTimeOrder = + resultingRate.packageSubmissions.reverse() console.info( 'ALL First REvisions: ', - JSON.stringify(revisionsInTimeOrder, null, ' ') + JSON.stringify(submissionsInTimeOrder, null, ' ') ) // Each Revision needs a Reason, one of the rates or revisions associated with it should have changed and why. - - expect(revisionsInTimeOrder).toHaveLength(8) - expect(revisionsInTimeOrder[0].contractRevisions).toHaveLength(0) - expect(revisionsInTimeOrder[0].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[0].submitInfo?.updatedReason).toBe( - 'initial rate submit' + expect(submissionsInTimeOrder).toHaveLength(8) + expect(submissionsInTimeOrder[0].contractRevisions).toHaveLength(1) + // expect(submissionsInTimeOrder[0].unlockInfo).toBeUndefined() + expect(submissionsInTimeOrder[0].submitInfo?.updatedReason).toBe( + 'First Contract Submit' ) - expect(revisionsInTimeOrder[1].contractRevisions).toHaveLength(1) - expect(revisionsInTimeOrder[1].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[1].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[1].contractRevisions).toHaveLength(2) + // expect(submissionsInTimeOrder[1].unlockInfo).toBeUndefined() + expect(submissionsInTimeOrder[1].submitInfo?.updatedReason).toBe( 'Contract Submit' ) - expect(revisionsInTimeOrder[2].contractRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[2].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[2].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[2].contractRevisions).toHaveLength(3) + // expect(submissionsInTimeOrder[2].unlockInfo).toBeUndefined() + expect(submissionsInTimeOrder[2].submitInfo?.updatedReason).toBe( 'ContractSubmit 2' ) - expect(revisionsInTimeOrder[3].contractRevisions).toHaveLength(3) - expect(revisionsInTimeOrder[3].unlockInfo).toBeUndefined() - expect(revisionsInTimeOrder[3].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[3].contractRevisions).toHaveLength(4) + // expect(submissionsInTimeOrder[3].unlockInfo).toBeUndefined() + expect(submissionsInTimeOrder[3].submitInfo?.updatedReason).toBe( '3.0 create' ) - expect(revisionsInTimeOrder[4].contractRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[4].unlockInfo?.updatedReason).toBe( - 'unlock for 2.1 remove' - ) - expect(revisionsInTimeOrder[4].unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect(revisionsInTimeOrder[4].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[4].contractRevisions).toHaveLength(3) + // expect(submissionsInTimeOrder[4].unlockInfo?.updatedReason).toBe( + // 'unlock for 2.1 remove' + // ) + // expect(submissionsInTimeOrder[4].unlockInfo?.updatedBy).toBe( + // 'zuko@example.com' + // ) + expect(submissionsInTimeOrder[4].submitInfo?.updatedReason).toBe( '2.1 remove' ) - expect(revisionsInTimeOrder[5].contractRevisions).toHaveLength(2) + expect(submissionsInTimeOrder[5].contractRevisions).toHaveLength(3) expect( - revisionsInTimeOrder[5].contractRevisions[1].formData + submissionsInTimeOrder[5].contractRevisions[2].formData .submissionDescription ).toBe('onepointone') - expect(revisionsInTimeOrder[5].unlockInfo?.updatedReason).toBe( - 'unlock for 1.1' - ) - expect(revisionsInTimeOrder[5].submitInfo?.updatedReason).toBe( + // expect(submissionsInTimeOrder[5].unlockInfo?.updatedReason).toBe( + // 'unlock for 1.1' + // ) + expect(submissionsInTimeOrder[5].submitInfo?.updatedReason).toBe( '1.1 new name' ) - expect(revisionsInTimeOrder[6].contractRevisions).toHaveLength(2) - expect(revisionsInTimeOrder[6].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[6].contractRevisions).toHaveLength(3) + expect(submissionsInTimeOrder[6].submitInfo?.updatedReason).toBe( 'Submitting A.1' ) - expect(revisionsInTimeOrder[7].contractRevisions).toHaveLength(1) - expect(revisionsInTimeOrder[7].formData).toEqual( + expect(submissionsInTimeOrder[7].contractRevisions).toHaveLength(1) + expect(submissionsInTimeOrder[7].rateRevision.formData).toEqual( expect.objectContaining({ rateCertificationName: 'one contract', rateDateCertified: undefined, @@ -438,7 +439,7 @@ describe('findRate', () => { rateType: 'AMENDMENT', }) ) - expect(revisionsInTimeOrder[7].submitInfo?.updatedReason).toBe( + expect(submissionsInTimeOrder[7].submitInfo?.updatedReason).toBe( 'Submitting A.2' ) @@ -452,10 +453,10 @@ describe('findRate', () => { throw contract1fetched } - expect(contract1fetched.revisions).toHaveLength(4) - expect(contract1fetched.revisions[0].submitInfo?.updatedReason).toBe( - 'Submitting A.2' - ) + expect(contract1fetched.packageSubmissions).toHaveLength(4) + expect( + contract1fetched.packageSubmissions[0].submitInfo?.updatedReason + ).toBe('Submitting A.2') }) // This test mimics the way rates are created, updated, and disconnected using the app today. @@ -706,8 +707,7 @@ describe('findRate', () => { ).toBe('resubmit without contract') }) - // eslint-disable-next-line jest/no-disabled-tests - it.skip('matches contract revision to rate revision with independent rate submit and unlocks', async () => { + it('matches contract revision to rate revision with independent rate submit and unlocks', async () => { const client = await sharedTestPrismaClient() const stateUser = await client.user.create({ @@ -843,7 +843,7 @@ describe('findRate', () => { 'submit contract revision 1.0' ) - // Unlock both contract and rate and resubmit + // Unlock rate and resubmit must( await unlockRate(client, { rateID, @@ -875,174 +875,209 @@ describe('findRate', () => { 'submit contract revision 1.1' ) - // TODO: This stuff only really makes sense once we have package history + // create a new contract linked to it and submit many times + const draftContract2Data = mockInsertContractArgs({ + submissionDescription: 'two contract', + }) + const draft2Contract = must( + await insertDraftContract(client, draftContract2Data) + ) - // // Multiple contract unlocks and resubmits - // must( - // await unlockContract(client, { - // contractID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock contract revision 1.2', - // }) - // ) - // must( - // await submitContract(client, { - // contractID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit contract revision 1.3', - // }) - // ) - // must( - // await unlockContract(client, { - // contractID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock contract revision 1.3', - // }) - // ) - // must( - // await submitContract(client, { - // contractID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit contract revision 1.4', - // }) - // ) - // must( - // await unlockContract(client, { - // contractID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock contract revision 1.4', - // }) - // ) - // must( - // await submitContract(client, { - // contractID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit contract revision 1.5', - // }) - // ) + const updatedContract2 = must( + await updateDraftContractRates(client, { + contractID: draft2Contract.id, + rateUpdates: { + create: [], + update: [], + link: [ + { + rateID: rateID, + ratePosition: 1, + }, + ], + unlink: [], + delete: [], + }, + }) + ) + + // Submit contract + must( + await submitContract(client, { + contractID: updatedContract2.id, + submittedByUserID: stateUser.id, + submittedReason: 'submit contract revision 2.0', + }) + ) - // // Fetch fresh data - // submittedRate = must(await findRateWithHistory(client, rateID)) + const contract2ID = updatedContract2.id - // // Expect latest rate revision to be 1.2 and have contract revision 1.5 - // expect(submittedRate.revisions[0].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.2' - // ) - // expect( - // submittedRate.revisions[0].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.5') - - // // Expect previous rate revisions to still be connected to the same contract revision - // expect(submittedRate.revisions[1].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.1' - // ) - // expect( - // submittedRate.revisions[1].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.1') - // expect(submittedRate.revisions[2].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.0' - // ) - // expect( - // submittedRate.revisions[2].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.1') - - // // 3 rate unlocks and resubmits - // must( - // await unlockRate(client, { - // rateID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock rate revision 1.2', - // }) - // ) - // must( - // await submitRate(client, { - // rateID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit rate revision 1.3', - // }) - // ) - // must( - // await unlockRate(client, { - // rateID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock rate revision 1.3', - // }) - // ) - // must( - // await submitRate(client, { - // rateID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit rate revision 1.4', - // }) - // ) - // must( - // await unlockRate(client, { - // rateID, - // unlockedByUserID: cmsUser.id, - // unlockReason: 'unlock rate revision 1.4', - // }) - // ) - // must( - // await submitRate(client, { - // rateID, - // submittedByUserID: stateUser.id, - // submittedReason: 'submit rate revision 1.5', - // }) - // ) + // Multiple contract unlocks and resubmits + must( + await unlockContract(client, { + contractID: contract2ID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock contract revision 2.0', + }) + ) + must( + await submitContract(client, { + contractID: contract2ID, + submittedByUserID: stateUser.id, + submittedReason: 'submit contract revision 2.1', + }) + ) + must( + await unlockContract(client, { + contractID: contract2ID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock contract revision 2.1', + }) + ) + must( + await submitContract(client, { + contractID: contract2ID, + submittedByUserID: stateUser.id, + submittedReason: 'submit contract revision 2.2', + }) + ) + must( + await unlockContract(client, { + contractID: contract2ID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock contract revision 2.2', + }) + ) + must( + await submitContract(client, { + contractID: contract2ID, + submittedByUserID: stateUser.id, + submittedReason: 'submit contract revision 2.3', + }) + ) - // // Fetch fresh data - // submittedRate = must(await findRateWithHistory(client, rateID)) + // Fetch fresh data + submittedRate = must(await findRateWithHistory(client, rateID)) - // // Expect to have 6 revisions, 3 additional from 3 unlocks and resubmits - // expect(submittedRate.revisions).toHaveLength(6) + // Expect latest rate revision to be 1.2 and have contract revision 2.3 + expect( + submittedRate.packageSubmissions[0].submitInfo?.updatedReason + ).toBe('submit contract revision 2.3') + expect( + submittedRate.packageSubmissions[0].rateRevision.submitInfo + ?.updatedReason + ).toBe('submit rate revision 1.2') + expect( + submittedRate.packageSubmissions[0].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') + expect( + submittedRate.packageSubmissions[0].contractRevisions[1].submitInfo + ?.updatedReason + ).toBe('submit contract revision 2.3') - // // Expect three latest revisions to have contract version 1.5 - // expect(submittedRate.revisions[0].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.5' - // ) - // expect( - // submittedRate.revisions[0].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.5') - // expect(submittedRate.revisions[1].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.4' - // ) - // expect( - // submittedRate.revisions[1].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.5') - // expect(submittedRate.revisions[2].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.3' - // ) - // expect( - // submittedRate.revisions[2].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.5') - - // // Expect earliest 3 rate revisions to have the same contract revision as before. - // expect(submittedRate.revisions[3].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.2' - // ) - // expect( - // submittedRate.revisions[3].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.5') - // expect(submittedRate.revisions[4].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.1' - // ) - // expect( - // submittedRate.revisions[4].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.1') - // expect(submittedRate.revisions[5].submitInfo?.updatedReason).toBe( - // 'submit rate revision 1.0' - // ) - // expect( - // submittedRate.revisions[5].contractRevisions[0].submitInfo - // ?.updatedReason - // ).toBe('submit contract revision 1.1') + // Expect previous rate revisions to still be connected to the same contract revision + expect( + submittedRate.packageSubmissions[1].submitInfo?.updatedReason + ).toBe('submit contract revision 2.2') + expect( + submittedRate.packageSubmissions[1].rateRevision.submitInfo + ?.updatedReason + ).toBe('submit rate revision 1.2') + expect( + submittedRate.packageSubmissions[1].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') + expect( + submittedRate.packageSubmissions[1].contractRevisions[1].submitInfo + ?.updatedReason + ).toBe('submit contract revision 2.2') + + expect( + submittedRate.packageSubmissions[2].submitInfo?.updatedReason + ).toBe('submit contract revision 2.1') + expect( + submittedRate.packageSubmissions[2].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') + + // 3 rate unlocks and resubmits + must( + await unlockRate(client, { + rateID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock rate revision 1.2', + }) + ) + must( + await submitRate(client, { + rateID, + submittedByUserID: stateUser.id, + submittedReason: 'submit rate revision 1.3', + }) + ) + must( + await unlockRate(client, { + rateID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock rate revision 1.3', + }) + ) + must( + await submitRate(client, { + rateID, + submittedByUserID: stateUser.id, + submittedReason: 'submit rate revision 1.4', + }) + ) + must( + await unlockRate(client, { + rateID, + unlockedByUserID: cmsUser.id, + unlockReason: 'unlock rate revision 1.4', + }) + ) + must( + await submitRate(client, { + rateID, + submittedByUserID: stateUser.id, + submittedReason: 'submit rate revision 1.5', + }) + ) + + // Fetch fresh data + submittedRate = must(await findRateWithHistory(client, rateID)) + + // Expect to have 6 revisions, 3 additional from 3 unlocks and resubmits + expect(submittedRate.packageSubmissions).toHaveLength(11) + + // Expect three latest revisions to have contract version 1.5 + expect( + submittedRate.packageSubmissions[0].submitInfo?.updatedReason + ).toBe('submit rate revision 1.5') + expect( + submittedRate.packageSubmissions[0].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') + expect( + submittedRate.packageSubmissions[0].contractRevisions[1].submitInfo + ?.updatedReason + ).toBe('submit contract revision 2.3') + + expect( + submittedRate.packageSubmissions[1].submitInfo?.updatedReason + ).toBe('submit rate revision 1.4') + expect( + submittedRate.packageSubmissions[1].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') + + expect( + submittedRate.packageSubmissions[2].submitInfo?.updatedReason + ).toBe('submit rate revision 1.3') + expect( + submittedRate.packageSubmissions[2].contractRevisions[0].submitInfo + ?.updatedReason + ).toBe('submit contract revision 1.1') }) }) diff --git a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts index d38429c91e..375b54868b 100644 --- a/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts @@ -1,6 +1,5 @@ import type { ContractType, - ContractRevisionWithRatesType, ContractRevisionType, RateRevisionType, } from '../../domain-models/contractAndRates' @@ -11,11 +10,7 @@ import { DRAFT_PARENT_PLACEHOLDER, rateWithoutDraftContractsToDomainModel, } from './parseRateWithHistory' -import type { - RateRevisionTableWithFormData, - ContractRevisionTableWithFormData, - UpdateInfoTableWithUpdater, -} from './prismaSharedContractRateHelpers' +import type { ContractRevisionTableWithFormData } from './prismaSharedContractRateHelpers' import { setDateAddedForContractRevisions, setDateAddedForRateRevisions, @@ -27,7 +22,6 @@ import { import { contractFormDataToDomainModel, convertUpdateInfoToDomainModel, - ratesRevisionsToDomainModel, getContractRateStatus, } from './prismaSharedContractRateHelpers' import type { ContractTableWithoutDraftRates } from './prismaSubmittedContractHelpers' @@ -80,41 +74,6 @@ function parseContractWithHistory( return parseContract.data } -// ContractRevisionSet is for the internal building of individual revisions -// we convert them into ContractRevisions to return them -interface ContractRevisionSet { - contractRev: ContractRevisionTableWithFormData - submitInfo: UpdateInfoTableWithUpdater - unlockInfo: UpdateInfoTableWithUpdater | undefined - rateRevisions: RateRevisionTableWithFormData[] -} - -function contractSetsToDomainModel( - revisions: ContractRevisionSet[] -): ContractRevisionWithRatesType[] | Error { - const contractRevisions = [] - - for (const revision of revisions) { - const rateRevisions = ratesRevisionsToDomainModel( - revision.rateRevisions - ) - - if (rateRevisions instanceof Error) { - return rateRevisions - } - - contractRevisions.push({ - ...contractRevisionToDomainModel(revision.contractRev), - rateRevisions, - // override this contractRevisions's update infos with the one that caused this revision to be created. - submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo), - unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo), - }) - } - - return contractRevisions -} - function contractRevisionToDomainModel( revision: ContractRevisionTableWithFormData ): ContractRevisionType { @@ -130,12 +89,6 @@ function contractRevisionToDomainModel( } } -function contractRevisionsToDomainModels( - contractRevisions: ContractRevisionTableWithFormData[] -): ContractRevisionType[] { - return contractRevisions.map((crev) => contractRevisionToDomainModel(crev)) -} - // contractWithHistoryToDomainModelWithoutRates constructs a history for this particular contract including changes to all of its // revisions and all related rate revisions, including added and removed rates, but eliding any draft rates to break the recursion chain. function contractWithHistoryToDomainModelWithoutRates( @@ -144,12 +97,12 @@ function contractWithHistoryToDomainModelWithoutRates( // 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: ContractRevisionType | Error | undefined = undefined + let draftRevision: ContractRevisionType | undefined = undefined + const submittedRevisions: ContractRevisionType[] = [] - for (const [contractRevIndex, contractRev] of contractRevisions.entries()) { + for (const contractRev of contractRevisions) { // If we have a draft revision // We set the draft revision aside, format it properly if (!contractRev.submitInfo) { @@ -160,144 +113,13 @@ function contractWithHistoryToDomainModelWithoutRates( ) } - draftRevision = { - id: contractRev.id, - contract: contractRev.contract, - createdAt: contractRev.createdAt, - updatedAt: contractRev.updatedAt, - unlockInfo: convertUpdateInfoToDomainModel( - contractRev.unlockInfo - ), - formData: contractFormDataToDomainModel(contractRev), - } - + draftRevision = contractRevisionToDomainModel(contractRev) // skip the rest of the processing continue } - - // This initial entry is the first history record of this contract revision. - // Then the for loop with it's rateRevisions are additional history records for each change in rate revisions. - // This is why allRevisionSets could have more entries than contract revisions. - const initialEntry: ContractRevisionSet = { - contractRev, - submitInfo: contractRev.submitInfo, - unlockInfo: contractRev.unlockInfo || undefined, - rateRevisions: [], - } - - // This code below was used to construct rate change history and add into our contract revision history by pushing - // new contract revisions into the array. This however caused issues with the frontend apollo cache because we - // used duplicate contract revision ids to create new revisions for rate changes. - // For now, we are commenting out the code until we are ready for this feature and leaving it intact. - - // let lastEntry = initialEntry - // // Now we construct a revision history for each change in rate revisions. - // // go through every rate revision in the join table in time order and construct a revisionSet - // // with (or without) the new rate revision in it. - // for (const rateRev of contractRev.rateRevisions) { - // if (!rateRev.rateRevision.submitInfo) { - // return new Error( - // 'Programming Error: a contract is associated with an unsubmitted rate' - // ) - // } - // - // // if it's from before this contract was submitted, it's there at the beginning. - // if ( - // rateRev.rateRevision.submitInfo.updatedAt <= - // contractRev.submitInfo.updatedAt - // ) { - // if (!rateRev.isRemoval) { - // initialEntry.rateRevisions.push(rateRev.rateRevision) - // } - // } else { - // // if after, then it's always a new entry in the list - // let lastRates = [...lastEntry.rateRevisions] - // - // // take out the previous rate revision this revision supersedes - // lastRates = lastRates.filter( - // (r) => r.rateID !== rateRev.rateRevision.rateID - // ) - // // an isRemoval entry indicates that this rate was removed from this contract. - // if (!rateRev.isRemoval) { - // lastRates.push(rateRev.rateRevision) - // } - // - // const newRev: ContractRevisionSet = { - // contractRev, - // submitInfo: rateRev.rateRevision.submitInfo, - // unlockInfo: rateRev.rateRevision.unlockInfo || undefined, - // rateRevisions: lastRates, - // } - // - // lastEntry = newRev - // allRevisionSets.push(newRev) - // } - // } - - /** - * Below a temporary approach to finding the matching rate revision to the contract revision. The correct way - * for this is to build the actual contract and rate history. This will be done in the Rate Change History epic - * https://qmacbis.atlassian.net/browse/MCR-3607 - * - * The approach to finding the **single** rate revision for the submitted contract revision is to find - * the latest rate revision submitted before the next contract revision unlock date. The latest rate revision - * and not the one submitted with the contract, because rates can be unlocked and resubmitted independently of - * the contract. - * - * The idea is that once a contract is unlocked again, the new contract revision created is now the "active" - * revision with most up-to-date data and previous submitted contract revision is now historical and changes - * should not be reflected on it, including rate changes. - **/ - - // Get next contract revision in the array - const nextContractRev: ContractRevisionTableWithFormData | undefined = - contractRevisions[contractRevIndex + 1] - - // Reverse rateRevisions so it is in DESC order. - const rateRevisions = - contractRev.relatedSubmisions[0].submissionPackages - - for (const rateRev of rateRevisions) { - if (!rateRev.rateRevision.submitInfo) { - return new Error( - 'Programming Error: a contract is associated with an unsubmitted rate' - ) - } - - // Check if rate revision submitted date is earlier than the proceeding contract revisions unlocked date. - // If nextContractRev does not exist, then there is no date constraint. - const isRateSubmittedDateValid = nextContractRev?.unlockInfo - ? rateRev.rateRevision.submitInfo.updatedAt.getTime() < - nextContractRev.unlockInfo.updatedAt.getTime() - : true - - // Does initial entries rateRevisions already include a revision for this rate - const isRateIncluded = !!initialEntry.rateRevisions.find( - (rr) => rr.rateID === rateRev.rateRevision.rateID - ) - - // Rate revision that belong to this contract revision has to be: - // - Submitted before the next contract rev unlock date - // - Not already in the initial entry. We are looping through this in desc order, so the first rate rev is the latest. - // - Not removed from the contract - if (isRateSubmittedDateValid && !isRateIncluded) { - // unshift rate revision into entries to asc order - initialEntry.rateRevisions.unshift(rateRev.rateRevision) - } - } - - allRevisionSets.push(initialEntry) - } - - const revisions = contractSetsToDomainModel(allRevisionSets) - - if (revisions instanceof Error) { - return new Error( - `error converting contract with id ${contract.id} to domain models: ${draftRevision}` - ) + submittedRevisions.push(contractRevisionToDomainModel(contractRev)) } - // New C+R package history code // Every revision has a set of submissions it was part of. const packageSubmissions: ContractPackageSubmissionType[] = [] for (const revision of contract.revisions) { @@ -371,7 +193,7 @@ function contractWithHistoryToDomainModelWithoutRates( stateCode: contract.stateCode, stateNumber: contract.stateNumber, draftRevision, - revisions: revisions.reverse(), + revisions: submittedRevisions.reverse(), packageSubmissions: packageSubmissions.reverse(), } } @@ -423,7 +245,6 @@ function contractWithHistoryToDomainModel( export { parseContractWithHistory, contractRevisionToDomainModel, - contractRevisionsToDomainModels, contractWithHistoryToDomainModel, contractWithHistoryToDomainModelWithoutRates, arrayOrFirstError, diff --git a/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts index b111ad9c13..b1e33c3c13 100644 --- a/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts @@ -1,5 +1,4 @@ import type { - RateRevisionWithContractsType, RateType, RateRevisionType, } from '../../domain-models/contractAndRates' @@ -10,16 +9,11 @@ import type { } from '../../domain-models/contractAndRates/packageSubmissions' import type { RateWithoutDraftContractsType } from '../../domain-models/contractAndRates/baseContractRateTypes' import { - contractRevisionsToDomainModels, arrayOrFirstError, contractWithHistoryToDomainModelWithoutRates, contractRevisionToDomainModel, } from './parseContractWithHistory' -import type { - ContractRevisionTableWithFormData, - RateRevisionTableWithFormData, - UpdateInfoTableWithUpdater, -} from './prismaSharedContractRateHelpers' +import type { RateRevisionTableWithFormData } from './prismaSharedContractRateHelpers' import { convertUpdateInfoToDomainModel, getContractRateStatus, @@ -49,50 +43,11 @@ function parseRateWithHistory(rate: RateTableFullPayload): RateType | Error { return parseRate.data } -// RateRevisionSet is for the internal building of individual revisions -// we convert them into RateRevisions to return them -interface RateRevisionSet { - rateRev: RateRevisionTableWithFormData - submitInfo: UpdateInfoTableWithUpdater - unlockInfo: UpdateInfoTableWithUpdater | undefined - contractRevs: ContractRevisionTableWithFormData[] -} - -function rateSetsToDomainModel( - entries: RateRevisionSet[] -): RateRevisionWithContractsType[] | Error { - const revisions: RateRevisionWithContractsType[] = [] - - for (const entry of entries) { - const domainRateRevision = rateRevisionToDomainModel(entry.rateRev) - - if (domainRateRevision instanceof Error) { - return domainRateRevision - } - - revisions.push({ - ...domainRateRevision, - contractRevisions: contractRevisionsToDomainModels( - entry.contractRevs - ), - - // override this contractRevisions's update infos with the one that caused this revision to be created. - submitInfo: convertUpdateInfoToDomainModel(entry.submitInfo), - unlockInfo: convertUpdateInfoToDomainModel(entry.unlockInfo), - }) - } - - return revisions -} function rateRevisionToDomainModel( revision: RateRevisionTableWithFormData -): RateRevisionType | Error { +): RateRevisionType { const formData = rateFormDataToDomainModel(revision) - if (formData instanceof Error) { - return formData - } - return { id: revision.id, rateID: revision.rateID, @@ -104,24 +59,6 @@ function rateRevisionToDomainModel( } } -function rateRevisionsToDomainModels( - rateRevisions: RateRevisionTableWithFormData[] -): RateRevisionType[] | Error { - const domainRateRevisions: RateRevisionType[] = [] - - for (const rateRevision of rateRevisions) { - const domainRateRevision = rateRevisionToDomainModel(rateRevision) - - if (domainRateRevision instanceof Error) { - return domainRateRevision - } - - domainRateRevisions.push(domainRateRevision) - } - - return domainRateRevisions -} - const DRAFT_PARENT_PLACEHOLDER = 'DRAFT_PARENT_REPLACE_ME' // rateWithoutDraftContractsToDomainModel constructs a history for this particular contract including changes to all of its @@ -132,12 +69,11 @@ function rateWithoutDraftContractsToDomainModel( // 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: RateRevisionType | Error | undefined = undefined - for (const [, rateRev] of rateRevisions.entries()) { + let draftRevision: RateRevisionType | undefined = undefined + const submittedRevisions: RateRevisionType[] = [] + for (const rateRev of rateRevisions) { // If we have a draft revision // We set the draft revision aside, format it properly if (!rateRev.submitInfo) { @@ -148,68 +84,13 @@ function rateWithoutDraftContractsToDomainModel( ) } - draftRevision = { - id: rateRev.id, - rateID: rateRev.rateID, - // rate: rateRev.rate, // not symmetric - this exists on contract draft revision but not on rate - createdAt: rateRev.createdAt, - updatedAt: rateRev.updatedAt, - unlockInfo: convertUpdateInfoToDomainModel(rateRev.unlockInfo), - formData: rateFormDataToDomainModel(rateRev), - } + draftRevision = rateRevisionToDomainModel(rateRev) // skip the rest of the processing continue } - /** - * Below a temporary approach to finding the matching rate revision to the contract revision. The correct way - * for this is to build the actual contract and rate history. This will be done in the Rate Change History epic - - * The approach to finding the **single** rate revision for the submitted contract revision is to find - * the latest contract revision submitted before the next rate revision unlock date. The latest contract revision - * and not the one submitted with the rate, because contracts can be unlocked and resubmitted independently of - * the rate. - * - * The idea is that once a rate is unlocked again, the new rate revision created is now the "active" - * revision with most up-to-date data and previous submitted rate revision is now historical and changes - * should not be reflected on it, including contract changes. - **/ - - // New Way: post-migration - if (rateRev.relatedSubmissions.length > 0) { - // we aren't returning individual submission packages, so we just want to make sure that - // each rate revision we return has the most up to date set of related contract submissions associated with it - - const mostRecentSubmission = - rateRev.relatedSubmissions[ - rateRev.relatedSubmissions.length - 1 - ] - - const mostRecentPackageContracts = - mostRecentSubmission.submissionPackages.filter( - (p) => p.rateRevision.rateID === rateRev.rateID - ) - - const mostRecentContractRevs = mostRecentPackageContracts.map( - (p) => p.contractRevision - ) - - allEntries.push({ - rateRev, - submitInfo: rateRev.submitInfo, - unlockInfo: rateRev.unlockInfo || undefined, - contractRevs: mostRecentContractRevs, - }) - } - } - - const revisions = rateSetsToDomainModel(allEntries) - - if (revisions instanceof Error) { - return new Error( - `error converting rate with id ${rate.id} to domain models: ${draftRevision}` - ) + submittedRevisions.push(rateRevisionToDomainModel(rateRev)) } // New C+R package history code @@ -299,11 +180,12 @@ function rateWithoutDraftContractsToDomainModel( } } + // TODO: why are we handling this differently from how we're doing dates in parseContractWithHistory // handle legacy revisions dateAdded on documents // get references to rate revision in submission order and // reset the document dateAdded dates accordingly. const firstSeenDate: { [sha: string]: Date } = {} - for (const rateRev of revisions) { + for (const rateRev of submittedRevisions) { const sinceDate = rateRev.submitInfo?.updatedAt || rateRev.updatedAt if (rateRev.formData.rateDocuments) { for (const doc of rateRev.formData.rateDocuments) { @@ -333,7 +215,7 @@ function rateWithoutDraftContractsToDomainModel( withdrawInfo: convertUpdateInfoToDomainModel(rate.withdrawInfo), stateNumber: rate.stateNumber, draftRevision, - revisions: revisions.reverse(), + revisions: submittedRevisions.reverse(), packageSubmissions: packageSubmissions.reverse(), } } @@ -372,8 +254,7 @@ function rateWithHistoryToDomainModel( // set parent id for a draft when parsing it. We fix it here. if (rateWithoutContracts.parentContractID === DRAFT_PARENT_PLACEHOLDER) { if (draftContractsOrError.length !== 1) { - const msg = - 'programming error: its an unsubmitted rate with not one draft contract' + const msg = `programming error: its an unsubmitted rate with ${draftContractsOrError.length} draft contracts` console.error(msg) return new Error(msg) } @@ -390,7 +271,6 @@ function rateWithHistoryToDomainModel( export { parseRateWithHistory, rateRevisionToDomainModel, - rateRevisionsToDomainModels, rateWithHistoryToDomainModel, rateWithoutDraftContractsToDomainModel, DRAFT_PARENT_PLACEHOLDER, diff --git a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts index 811930d365..b5f8acf0b5 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts @@ -129,8 +129,7 @@ type RateRevisionTableWithFormData = Prisma.RateRevisionTableGetPayload<{ }> function rateFormDataToDomainModel( - rateRevision: RateRevisionTableWithFormData, - previousRevision?: RateRevisionTableWithFormData + rateRevision: RateRevisionTableWithFormData ): RateFormDataType { const packagesWithSharedRateCerts = [] let statePrograms: ProgramType[] | Error | undefined = undefined @@ -317,8 +316,7 @@ type ContractRevisionTableWithFormData = }> function contractFormDataToDomainModel( - contractRevision: ContractRevisionTableWithFormData, - previousRevision?: ContractRevisionTableWithFormData + contractRevision: ContractRevisionTableWithFormData ): ContractFormDataType { return { submissionType: contractRevision.submissionType, diff --git a/services/app-api/src/postgres/contractAndRates/submitContractAndOrRates.ts b/services/app-api/src/postgres/contractAndRates/submitContractAndOrRates.ts index eda0a7787e..257b10b2da 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContractAndOrRates.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContractAndOrRates.ts @@ -310,7 +310,7 @@ async function submitContractAndOrRates( if ( !currentSubmittedRateConnections[ submittedRateRev.rateID - ].includes(previousConnection.contractRevision.contractID) + ]?.includes(previousConnection.contractRevision.contractID) ) { // this previous submission was connected to a now disconnected rate. submissionRelatedContractRevs.push( diff --git a/services/app-api/src/postgres/contractAndRates/submitRate.test.ts b/services/app-api/src/postgres/contractAndRates/submitRate.test.ts index e7dca682de..16108504e7 100644 --- a/services/app-api/src/postgres/contractAndRates/submitRate.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitRate.test.ts @@ -90,7 +90,7 @@ describe('submitRate', () => { // expect updated and submitted rate revision to be on submitted contract revision expect( - submittedContract.revisions[0].rateRevisions[0].formData + submittedContract.packageSubmissions[0].rateRevisions[0].formData ).toEqual( expect.objectContaining({ rateCertificationName: 'rate revision 1.0', diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts index f98417ecd6..0da78fef50 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.test.ts @@ -113,7 +113,7 @@ describe('unlockContract', () => { await updateDraftRate(client, { rateID: rate.id, formData: { rateCertificationName: 'Rate 2.0' }, - contractIDs: [], + contractIDs: [contract.id, contract2.id], }) ) @@ -136,7 +136,9 @@ describe('unlockContract', () => { draftContractRevTwo === undefined || draftContractTwoDraftRates === undefined ) { - throw Error('Unexpect error: draft contract missing draft revision') + throw Error( + 'Unexpected error: draft contract missing draft revision' + ) } // Contract should now have the latest rate revision @@ -239,10 +241,10 @@ describe('unlockContract', () => { }) ) // Latest revision is the last index - const latestContractRev = submittedContract.revisions[0] + const latestSubmission = submittedContract.packageSubmissions[0] // Expect rate to be connected to submitted contract - expect(latestContractRev.rateRevisions[0].id).toEqual( + expect(latestSubmission.rateRevisions[0].id).toEqual( submittedRate.revisions[0].id ) @@ -274,10 +276,11 @@ describe('unlockContract', () => { await findContractWithHistory(client, contract.id) ) // Latest revision is the last index - const latestResubmittedRev = submittedContract2.revisions[0] + const latestResubmittedPackage = + submittedContract2.packageSubmissions[0] // Expect latest contract revision to now be connected to latest rate revision - expect(latestResubmittedRev.rateRevisions[0].id).toEqual( + expect(latestResubmittedPackage.rateRevisions[0].id).toEqual( resubmittedRate.revisions[0].id ) }) diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts index afdae39be8..171edc35d0 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContractRates.ts @@ -196,6 +196,7 @@ async function updateDraftContractRatesInsideTransaction( } // unlink old data from disconnected rates + // This does nothing, just checks that the rates all exist. We are removing based on LINK being populated... for (const ru of args.rateUpdates.unlink) { const draftRev = await tx.rateRevisionTable.findFirst({ where: { diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftRate.test.ts b/services/app-api/src/postgres/contractAndRates/updateDraftRate.test.ts index a187d120cc..16beece28c 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftRate.test.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftRate.test.ts @@ -41,7 +41,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm2, - contractIDs: [], + contractIDs: [contract.id], }) ) @@ -119,7 +119,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm1, - contractIDs: [], + contractIDs: [contract.id], }) ) @@ -130,7 +130,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm2, - contractIDs: [], + contractIDs: [contract.id], }) ) @@ -149,7 +149,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm3, - contractIDs: [], + contractIDs: [contract.id], }) ) expect(draft3.draftRevision?.formData.rateCertificationName).toBe( @@ -232,7 +232,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm1, - contractIDs: [], + contractIDs: [contract.id], }) ) @@ -247,7 +247,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm2, - contractIDs: [], + contractIDs: [contract.id], }) ) @@ -265,7 +265,7 @@ describe('updateDraftRate', () => { await updateDraftRate(client, { rateID: rate.id, formData: draftRateForm3, - contractIDs: [], + contractIDs: [contract.id], }) ) expect(draft3.draftRevision?.formData.rateCertificationName).toBe( diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts b/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts index 685d0e5dc7..3007e18926 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts @@ -32,7 +32,7 @@ async function updateDraftRate( try { // Given all the Rates associated with this draft, find the most recent submitted to update. - const currentRev = await client.rateRevisionTable.findFirst({ + const draftRev = await client.rateRevisionTable.findFirst({ where: { rateID: rateID, submitInfoID: null, @@ -41,20 +41,63 @@ async function updateDraftRate( createdAt: 'desc', }, }) - if (!currentRev) { + if (!draftRev) { console.error('No Draft Rev!') return new Error('cant find a draft rev to submit') } + + // get all the now-related contracts, find the matching ratePosition if any, otherwise add one to the current max + const existingLinks = await client.draftRateJoinTable.findMany({ + where: { + contractID: { + in: args.contractIDs, + }, + }, + }) + + const newLinks = args.contractIDs.map((contractID) => { + let ratePosition = 1 + for (const link of existingLinks.filter( + (l) => l.contractID === contractID + )) { + if (rateID === args.rateID) { + ratePosition = link.ratePosition + break + } + if (link.ratePosition >= ratePosition) { + ratePosition = link.ratePosition + 1 + } + } + + return { + contractID, + ratePosition, + } + }) + // Clear all related resources on the revision // Then update resource, adjusting all simple fields and creating new linked resources for fields holding relationships to other day await client.rateRevisionTable.update({ where: { - id: currentRev.id, + id: draftRev.id, }, data: { ...prismaUpdateRateFormDataFromDomain(formData), }, }) + + await client.rateTable.update({ + where: { + id: rateID, + }, + data: { + draftContracts: { + deleteMany: {}, + create: newLinks, + }, + }, + }) + return findRateWithHistory(client, rateID) } catch (err) { console.error('Prisma error updating rate', err) diff --git a/services/app-api/src/resolvers/contract/contractRevisionResolver.ts b/services/app-api/src/resolvers/contract/contractRevisionResolver.ts index fe2dda2c76..8159392d05 100644 --- a/services/app-api/src/resolvers/contract/contractRevisionResolver.ts +++ b/services/app-api/src/resolvers/contract/contractRevisionResolver.ts @@ -7,6 +7,9 @@ export function contractRevisionResolver( store: Store ): Resolvers['ContractRevision'] { return { + contractID: (parent) => { + return parent.contract.id + }, contractName(parent: ContractRevisionType): string { const stateCode = parent.contract.stateCode const programsForContractState = store.findStatePrograms(stateCode) diff --git a/services/app-api/src/resolvers/contract/fetchContract.test.ts b/services/app-api/src/resolvers/contract/fetchContract.test.ts index 28a65d1f6c..ae558da281 100644 --- a/services/app-api/src/resolvers/contract/fetchContract.test.ts +++ b/services/app-api/src/resolvers/contract/fetchContract.test.ts @@ -63,9 +63,11 @@ describe('fetchContract', () => { expect(fetchDraftContractResult.errors).toBeUndefined() const draftContract = - fetchDraftContractResult.data?.fetchContract.contract.draftRevision + fetchDraftContractResult.data?.fetchContract.contract + const draftContractRev = draftContract.draftRevision - expect(draftContract.contractName).toMatch(/MCR-FL-\d{4}-NEMTMTM/) + expect(draftContractRev.contractName).toMatch(/MCR-FL-\d{4}-NEMTMTM/) + expect(draftContractRev.contractID).toBe(draftContract.id) }) it('returns a stable initially submitted at', async () => { diff --git a/services/app-api/src/resolvers/contract/unlockContract.ts b/services/app-api/src/resolvers/contract/unlockContract.ts index 3b58b55048..11cf000d38 100644 --- a/services/app-api/src/resolvers/contract/unlockContract.ts +++ b/services/app-api/src/resolvers/contract/unlockContract.ts @@ -139,10 +139,9 @@ export function unlockContractResolver( updatedReason: unlockedReason, } - const contractRev = unlockContractResult.revisions[0] const unlockContractCMSEmailResult = await emailer.sendUnlockContractCMSEmail( - contractRev, + unlockContractResult, updateInfo, stateAnalystsEmails, statePrograms @@ -150,7 +149,7 @@ export function unlockContractResolver( const unlockContractStateEmailResult = await emailer.sendUnlockContractStateEmail( - contractRev, + unlockContractResult, updateInfo, statePrograms, submitterEmails diff --git a/services/app-api/src/resolvers/rate/indexRates.test.ts b/services/app-api/src/resolvers/rate/indexRates.test.ts index ce9aa93206..176b7f45af 100644 --- a/services/app-api/src/resolvers/rate/indexRates.test.ts +++ b/services/app-api/src/resolvers/rate/indexRates.test.ts @@ -10,19 +10,12 @@ import { iterableCmsUsersMockData, testStateUser, } from '../../testHelpers/userHelpers' -import { formatGQLDate } from '../../common-code/dateHelpers' -import { - submitTestRate, - unlockTestRate, - updateTestRate, -} from '../../testHelpers' import { createAndSubmitTestContractWithRate, submitTestContract, createAndUpdateTestContractWithRate, } from '../../testHelpers/gqlContractHelpers' import { testS3Client } from '../../../../app-web/src/testHelpers/s3Helpers' -import statePrograms from '../../../../app-web/src/common-code/data/statePrograms.json' describe('indexRates', () => { describe.each(iterableCmsUsersMockData)( @@ -122,430 +115,6 @@ describe('indexRates', () => { expect(testRates).toHaveLength(0) }) - it('returns a rate with history with correct data in each revision', async () => { - const cmsUser = mockUser() - const server = await constructTestPostgresServer({ - ldService, - s3Client: mockS3, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService, - s3Client: mockS3, - }) - - const contract1 = - await createAndSubmitTestContractWithRate(server) - const contract2 = - await createAndSubmitTestContractWithRate(server) - - const firstRateID = - contract1.packageSubmissions[0].rateRevisions[0].rateID - const secondRateID = - contract2.packageSubmissions[0].rateRevisions[0].rateID - - // Unlock one to be rate edited in place - const firstRateUnlocked = await unlockTestRate( - cmsServer, - firstRateID, - 'Unlock to edit an existing rate' - ) - - const secondRateUnlocked = await unlockTestRate( - cmsServer, - secondRateID, - 'Unlock to edit an existing rate' - ) - - // update one with a new rate start and end date - const existingFormData = - firstRateUnlocked.draftRevision?.formData - expect(existingFormData).toBeDefined() - await updateTestRate(firstRateID, { - rateDateStart: new Date(Date.UTC(2025, 1, 1)), - rateDateEnd: new Date(Date.UTC(2027, 1, 1)), - }) - - // update the other with additional new rate - const existingFormData2 = - secondRateUnlocked.draftRevision?.formData - expect(existingFormData2).toBeDefined() - - const contract3 = - await createAndSubmitTestContractWithRate(server) - const newRateID = - contract3.packageSubmissions[0].rateRevisions[0].rateID - - // resubmit - const firstRateResubmitted = await submitTestRate( - server, - firstRateID, - 'Resubmit with edited rate description' - ) - const secondRateResubmitted = await submitTestRate( - server, - secondRateID, - 'Resubmit with an additional rate added' - ) - - // fetch rates and check that the latest data is correct - // index rates - const result = await cmsServer.executeOperation({ - query: INDEX_RATES, - }) - expect(result.errors).toBeUndefined() - const rates: Rate[] = result.data?.indexRates.edges.map( - (edge: RateEdge) => edge.node - ) - - const resubmittedWithEdits = rates.find((test: Rate) => { - return test.id === firstRateResubmitted.id - }) - const resubmittedUnchanged = rates.find((test: Rate) => { - return test.id == secondRateResubmitted.id - }) - const newlyAdded = rates.find((test: Rate) => { - return test.id === newRateID - }) - - if ( - !resubmittedWithEdits || - !resubmittedUnchanged || - !newlyAdded - ) { - throw new Error('we didnt find all the new rates in index') - } - - // Check resubmitted rate - most recent revision and previous - expect(resubmittedWithEdits.revisions).toHaveLength(2) - - expect( - resubmittedWithEdits.revisions[0].formData.rateDateStart - ).toBe(formatGQLDate(new Date(Date.UTC(2025, 1, 1)))) - expect( - resubmittedWithEdits.revisions[0].formData.rateDateEnd - ).toBe(formatGQLDate(new Date(Date.UTC(2027, 1, 1)))) - expect( - resubmittedWithEdits.revisions[0].submitInfo?.updatedReason - ).toBe('Resubmit with edited rate description') - expect( - resubmittedWithEdits.revisions[1].formData.rateDateStart - ).toBe('2024-01-01') - expect( - resubmittedWithEdits.revisions[1].formData.rateDateEnd - ).toBe('2025-01-01') - expect( - resubmittedWithEdits.revisions[1].submitInfo?.updatedReason - ).toBe('Initial submission') - - // check unchanged rate most recent revision and previous - expect(resubmittedUnchanged.revisions).toHaveLength(2) - expect( - resubmittedUnchanged.revisions[0].formData.rateDateStart - ).toBe('2024-01-01') - expect( - resubmittedUnchanged.revisions[0].formData.rateDateEnd - ).toBe('2025-01-01') - expect( - resubmittedUnchanged.revisions[0].submitInfo?.updatedReason - ).toBe('Resubmit with an additional rate added') - - expect( - resubmittedUnchanged.revisions[1].submitInfo?.updatedReason - ).toBe('Initial submission') - - expect( - resubmittedUnchanged.revisions[1].formData.rateDateStart - ).toBe('2024-01-01') - expect( - resubmittedUnchanged.revisions[1].formData.rateDateEnd - ).toBe('2025-01-01') - - // check newly added rate - expect(newlyAdded.revisions).toHaveLength(1) - expect(newlyAdded.revisions[0].formData.rateDateStart).toBe( - '2024-01-01' - ) - expect(newlyAdded.revisions[0].formData.rateDateEnd).toBe( - '2025-01-01' - ) - }) - - it('returns rates from specific state when stateCode passed in', async () => { - const stateServerFL = await constructTestPostgresServer({ - context: { - user: { ...testStateUser(), stateCode: 'FL' }, - }, - ldService, - s3Client: mockS3, - }) - const stateServerVA = await constructTestPostgresServer({ - context: { - user: { ...testStateUser(), stateCode: 'VA' }, - }, - ldService, - s3Client: mockS3, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: mockUser(), - }, - ldService, - s3Client: mockS3, - }) - const flPrograms = statePrograms.states.filter( - (program) => program.code === 'FL' - )[0].programs - const vaPrograms = statePrograms.states.filter( - (program) => program.code === 'VA' - )[0].programs - const contract1 = await createAndSubmitTestContractWithRate( - stateServerFL, - { stateCode: 'FL', programIDs: [flPrograms[0].id] } - ) - const contract2 = await createAndSubmitTestContractWithRate( - stateServerVA, - { stateCode: 'VA', programIDs: [vaPrograms[0].id] } - ) - - const flRateID = - contract1.packageSubmissions[0].rateRevisions[0].rateID - const vaRateID = - contract2.packageSubmissions[0].rateRevisions[0].rateID - - // fetch rates only with stateCode FL - - const result = await cmsServer.executeOperation({ - query: INDEX_RATES, - variables: { input: { stateCode: 'FL' } }, - }) - expect(result.errors).toBeUndefined() - const rates: Rate[] = result.data?.indexRates.edges.map( - (edge: RateEdge) => edge.node - ) - - const flRate = rates.find((test: Rate) => { - return test.id === flRateID - }) - const vaRate = rates.find((test: Rate) => { - return test.id == vaRateID - }) - - expect(flRate).toBeTruthy() - expect(vaRate).toBeFalsy() - - // fetch all rates - - const resultAllStates = await cmsServer.executeOperation({ - query: INDEX_RATES, - }) - expect(resultAllStates.errors).toBeUndefined() - const ratesAllStates: Rate[] = - resultAllStates.data?.indexRates.edges.map( - (edge: RateEdge) => edge.node - ) - - const flRateQueryingAllStates = ratesAllStates.find( - (test: Rate) => { - return test.id === flRateID - } - ) - const vaRateQueryingAllStates = ratesAllStates.find( - (test: Rate) => { - return test.id == vaRateID - } - ) - - expect(flRateQueryingAllStates).toBeTruthy() - expect(vaRateQueryingAllStates).toBeTruthy() - }) - - it('synthesizes the right statuses as a rate is submitted/unlocked/etc', async () => { - const cmsUser = mockUser() - const server = await constructTestPostgresServer({ - ldService, - s3Client: mockS3, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService, - s3Client: mockS3, - }) - - // First, create new submissions - const contract1 = - await createAndSubmitTestContractWithRate(server) - const contract2 = - await createAndSubmitTestContractWithRate(server) - const contract3 = - await createAndSubmitTestContractWithRate(server) - - const submittedRateID = - contract1.packageSubmissions[0].rateRevisions[0].rateID - const unlockedRateID = - contract2.packageSubmissions[0].rateRevisions[0].rateID - const relockedRateID = - contract3.packageSubmissions[0].rateRevisions[0].rateID - - // unlock two - await unlockTestRate(cmsServer, unlockedRateID, 'Test reason') - await unlockTestRate(cmsServer, relockedRateID, 'Test reason') - - // resubmit one - await submitTestRate( - server, - relockedRateID, - 'Test first resubmission' - ) - - // index rates - const result = await cmsServer.executeOperation({ - query: INDEX_RATES, - }) - const ratesIndex = result.data?.indexRates - expect(result.errors).toBeUndefined() - - // pull out test related rates and order them - const testRateIDs = [ - submittedRateID, - unlockedRateID, - relockedRateID, - ] - - const testRates: Rate[] = ratesIndex.edges - .map((edge: RateEdge) => edge.node) - .filter((test: Rate) => { - return testRateIDs.includes(test.id) - }) - - expect(testRates).toHaveLength(3) - - // organize test rates in a predictable order - testRates.sort((a, b) => { - if (testRateIDs.indexOf(a.id) > testRateIDs.indexOf(b.id)) { - return 1 - } else { - return -1 - } - }) - }) - - it('returns the right revisions as a rate is submitted/unlocked/etc', async () => { - const cmsUser = mockUser() - const server = await constructTestPostgresServer({ - ldService, - s3Client: mockS3, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService, - s3Client: mockS3, - }) - - // First, create new rates - const contract1 = - await createAndSubmitTestContractWithRate(server) - const contract2 = - await createAndSubmitTestContractWithRate(server) - const contract3 = - await createAndSubmitTestContractWithRate(server) - - const submittedRate2 = - contract1.packageSubmissions[0].rateRevisions[0] - const unlockedRate2 = - contract2.packageSubmissions[0].rateRevisions[0] - const relockedRate2 = - contract3.packageSubmissions[0].rateRevisions[0] - - // unlock two - await unlockTestRate( - cmsServer, - unlockedRate2.rateID, - 'Test reason' - ) - await unlockTestRate( - cmsServer, - relockedRate2.rateID, - 'Test reason' - ) - - // resubmit one - await submitTestRate( - server, - relockedRate2.rateID, - 'Test first resubmission' - ) - - // index rates - const result = await cmsServer.executeOperation({ - query: INDEX_RATES, - }) - - const ratesIndex = result.data?.indexRates - expect(result.errors).toBeUndefined() - - const submittedRateID = submittedRate2.rateID - const unlockedRateID = unlockedRate2.rateID - const resubmittedRateID = relockedRate2.rateID - - if (!submittedRateID || !unlockedRateID || !resubmittedRateID) { - throw new Error('Missing Rate ID') - } - - const testRateIDs = [ - submittedRateID, - unlockedRateID, - resubmittedRateID, - ] - - const ratesByID: { [id: string]: Rate } = {} - for (const rateEdge of ratesIndex.edges) { - if (testRateIDs.includes(rateEdge.node.id)) { - ratesByID[rateEdge.node.id] = rateEdge.node - } - } - - expect(Object.keys(ratesByID)).toHaveLength(3) - - const submittedRate = ratesByID[submittedRateID] - expect(submittedRate).toBeDefined() - expect(submittedRate.status).toBe('SUBMITTED') - - expect(submittedRate.draftRevision).toBeNull() - - expect(submittedRate.revisions).toHaveLength(1) - expect(submittedRate.revisions[0].submitInfo).toBeTruthy() - - const unlockedRate = ratesByID[unlockedRateID] - expect(unlockedRate).toBeDefined() - expect(unlockedRate.status).toBe('UNLOCKED') - - expect(unlockedRate.draftRevision).toBeDefined() - expect(unlockedRate.draftRevision?.submitInfo).toBeNull() - - expect(unlockedRate.revisions).toHaveLength(1) - expect(unlockedRate.revisions[0].submitInfo).toBeTruthy() - - const resubmittedRate = ratesByID[resubmittedRateID] - expect(resubmittedRate).toBeDefined() - expect(resubmittedRate.status).toBe('RESUBMITTED') - - expect(resubmittedRate.draftRevision).toBeNull() - - expect(resubmittedRate.revisions).toHaveLength(2) - expect(resubmittedRate.revisions[0].submitInfo).toBeTruthy() - }) - it('return a list of submitted rates from multiple states', async () => { const cmsUser = mockUser() const stateServer = await constructTestPostgresServer({ diff --git a/services/app-api/src/resolvers/rate/rateRevisionResolver.ts b/services/app-api/src/resolvers/rate/rateRevisionResolver.ts index 0aadb24df3..66bbf5b687 100644 --- a/services/app-api/src/resolvers/rate/rateRevisionResolver.ts +++ b/services/app-api/src/resolvers/rate/rateRevisionResolver.ts @@ -6,16 +6,9 @@ import { GraphQLError } from 'graphql' export function rateRevisionResolver(store: Store): Resolvers['RateRevision'] { return { - contractRevisions(parent) { - return parent.contractRevisions || [] - }, rate: async (parent, _args, context) => { const { ctx, tracer } = context - const span = tracer?.startSpan( - 'healthPlanPackageResolver.questions', - {}, - ctx - ) + const span = tracer?.startSpan('rateRevisionResolver.rate', {}, ctx) const rate = await store.findRateWithHistory(parent.rateID) if (rate instanceof Error) { diff --git a/services/app-api/src/resolvers/rate/submitRate.test.ts b/services/app-api/src/resolvers/rate/submitRate.test.ts index 1bffde7593..e821353334 100644 --- a/services/app-api/src/resolvers/rate/submitRate.test.ts +++ b/services/app-api/src/resolvers/rate/submitRate.test.ts @@ -293,7 +293,7 @@ describe('submitRate', () => { const subB0 = contractB0.packageSubmissions[0] const rate30 = subB0.rateRevisions[0] const TwoID = rate30.rateID - console.info('THREEID', TwoID) + console.info('TWOID', TwoID) expect(subB0.rateRevisions[0].rateID).toBe(TwoID) @@ -312,13 +312,23 @@ describe('submitRate', () => { // 4. make sure both rates return contract C in their list of revisions const rateOne = await fetchTestRateById(stateServer, OneID) - expect(rateOne.revisions).toHaveLength(1) - expect(rateOne.revisions[0].contractRevisions).toHaveLength(2) + expect(rateOne.packageSubmissions).toHaveLength(2) + expect(rateOne.packageSubmissions?.[0].contractRevisions).toHaveLength( + 2 + ) + expect(rateOne.packageSubmissions?.[1].contractRevisions).toHaveLength( + 1 + ) const rateTwo = await fetchTestRateById(stateServer, TwoID) - expect(rateTwo.revisions).toHaveLength(1) - expect(rateTwo.revisions[0].contractRevisions).toHaveLength(2) + expect(rateTwo.packageSubmissions).toHaveLength(2) + expect(rateTwo.packageSubmissions?.[0].contractRevisions).toHaveLength( + 2 + ) + expect(rateTwo.packageSubmissions?.[1].contractRevisions).toHaveLength( + 1 + ) // 5. unlock and resubmit A await unlockTestHealthPlanPackage( @@ -329,8 +339,13 @@ describe('submitRate', () => { const unlockedRateOne = await fetchTestRateById(stateServer, OneID) - expect(unlockedRateOne.revisions).toHaveLength(1) - expect(unlockedRateOne.revisions[0].contractRevisions).toHaveLength(2) + expect(unlockedRateOne.packageSubmissions).toHaveLength(2) + expect( + unlockedRateOne.packageSubmissions?.[0].contractRevisions + ).toHaveLength(2) + expect( + unlockedRateOne.packageSubmissions?.[1].contractRevisions + ).toHaveLength(1) await submitTestContract( stateServer, @@ -341,17 +356,17 @@ describe('submitRate', () => { // everything should have the latest const resubmittedRateOne = await fetchTestRateById(stateServer, OneID) - expect(resubmittedRateOne.revisions).toHaveLength(2) - expect(resubmittedRateOne.revisions[0].contractRevisions).toHaveLength( - 2 - ) + expect(resubmittedRateOne.packageSubmissions).toHaveLength(3) + expect( + resubmittedRateOne.packageSubmissions?.[0].contractRevisions + ).toHaveLength(2) const postResubmitRateTwo = await fetchTestRateById(stateServer, TwoID) - expect(postResubmitRateTwo.revisions).toHaveLength(1) - expect(postResubmitRateTwo.revisions[0].contractRevisions).toHaveLength( - 2 - ) + expect(postResubmitRateTwo.packageSubmissions).toHaveLength(2) + expect( + postResubmitRateTwo.packageSubmissions?.[0].contractRevisions + ).toHaveLength(2) const postSubmitC = await fetchTestContract(stateServer, contractC0.id) @@ -553,7 +568,6 @@ describe('submitRate', () => { const contractB0 = await submitTestContract(stateServer, draftB0.id) const subB0 = contractB0.packageSubmissions[0] - expect(subB0.rateRevisions[0].rateID).toBe(OneID) await unlockTestRate(cmsServer, OneID, 'unlock rate') @@ -574,8 +588,9 @@ describe('submitRate', () => { const fetchedRate = await fetchTestRateById(stateServer, OneID) const subs = fetchedRate.packageSubmissions - if (!subs) + if (!subs) { throw new Error('packageSubmissions are expected but missing') + } expect(subs).toHaveLength(3) expect(subs[0].submitInfo.updatedReason).toBe('final submit') diff --git a/services/app-api/src/testHelpers/emailerHelpers.ts b/services/app-api/src/testHelpers/emailerHelpers.ts index 542ec02268..48a6e817e4 100644 --- a/services/app-api/src/testHelpers/emailerHelpers.ts +++ b/services/app-api/src/testHelpers/emailerHelpers.ts @@ -5,7 +5,15 @@ import type { ProgramArgType, UnlockedHealthPlanFormDataType, } from '../common-code/healthPlanFormDataType' -import type { ContractRevisionWithRatesType, Question } from '../domain-models' +import type { + ContractRevisionType, + Question, + RatePackageSubmissionType, + RateRevisionType, + RateType, + UnlockedContractType, + UpdateInfoType, +} from '../domain-models' import { SESServiceException } from '@aws-sdk/client-ses' import { testSendSESEmail } from './awsSESHelpers' import { testCMSUser, testStateUser } from './userHelpers' @@ -131,9 +139,10 @@ export function mockMSState(): State { code: 'MS', } } + const mockContractRev = ( - submissionPartial?: Partial -): ContractRevisionWithRatesType => { + submissionPartial?: Partial +): ContractRevisionType => { return { createdAt: new Date('01/01/2021'), updatedAt: new Date('02/01/2021'), @@ -198,59 +207,124 @@ const mockContractRev = ( statutoryRegulatoryAttestation: undefined, statutoryRegulatoryAttestationDescription: undefined, }, - rateRevisions: [ - { - id: '12345', - rateID: '6789', - submitInfo: undefined, - unlockInfo: undefined, - createdAt: new Date(11 / 27 / 2023), - updatedAt: new Date(11 / 27 / 2023), - formData: { - id: 'test-id-1234', - rateID: 'test-id-1234', - rateType: 'NEW', - rateCapitationType: 'RATE_CELL', - rateDocuments: [ - { - s3URL: 's3://bucketname/key/test1', - name: 'foo', - sha256: 'fakesha', - dateAdded: new Date(11 / 27 / 2023), - }, - ], - supportingDocuments: [], - rateDateStart: new Date('01/01/2024'), - rateDateEnd: new Date('01/01/2025'), - rateDateCertified: new Date('01/01/2024'), - amendmentEffectiveDateStart: new Date('01/01/2024'), - amendmentEffectiveDateEnd: new Date('01/01/2025'), - rateProgramIDs: ['3fd36500-bf2c-47bc-80e8-e7aa417184c5'], - rateCertificationName: 'Rate Cert Name', - certifyingActuaryContacts: [ - { - actuarialFirm: 'DELOITTE', - name: 'Actuary Contact 1', - titleRole: 'Test Actuary Contact 1', - email: 'actuarycontact1@example.com', - }, - ], - addtlActuaryContacts: [], - actuaryCommunicationPreference: 'OACT_TO_ACTUARY', - packagesWithSharedRateCerts: [ - { - packageName: 'pkgName', - packageId: '12345', - packageStatus: 'SUBMITTED', - }, - ], - }, - }, - ], ...submissionPartial, } } +const mockRateRevision = ( + rateRevPartial?: Partial +): RateRevisionType => { + return { + id: '12345', + rateID: '6789', + submitInfo: undefined, + unlockInfo: undefined, + createdAt: new Date(11 / 27 / 2023), + updatedAt: new Date(11 / 27 / 2023), + formData: { + id: 'test-id-1234', + rateID: 'test-id-1234', + rateType: 'NEW', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 's3://bucketname/key/test1', + name: 'foo', + sha256: 'fakesha', + dateAdded: new Date(11 / 27 / 2023), + }, + ], + supportingDocuments: [], + rateDateStart: new Date('01/01/2024'), + rateDateEnd: new Date('01/01/2025'), + rateDateCertified: new Date('01/01/2024'), + amendmentEffectiveDateStart: new Date('01/01/2024'), + amendmentEffectiveDateEnd: new Date('01/01/2025'), + rateProgramIDs: ['3fd36500-bf2c-47bc-80e8-e7aa417184c5'], + rateCertificationName: 'Rate Cert Name', + certifyingActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Actuary Contact 1', + titleRole: 'Test Actuary Contact 1', + email: 'actuarycontact1@example.com', + }, + ], + addtlActuaryContacts: [], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + packagesWithSharedRateCerts: [ + { + packageName: 'pkgName', + packageId: '12345', + packageStatus: 'SUBMITTED', + }, + ], + }, + ...rateRevPartial, + } +} + +const mockRate = (ratePartial?: Partial): RateType => { + const submitInfo: UpdateInfoType = { + updatedAt: new Date('02/01/2021'), + updatedBy: 'someone@example.com', + updatedReason: 'Initial submission', + } + const rateRev = mockRateRevision({ + submitInfo, + }) + const contractRev = mockContractRev({ + id: 'test-contract-234', + submitInfo, + }) + const rateSubmission: RatePackageSubmissionType = { + submitInfo, + submittedRevisions: [rateRev, contractRev], + rateRevision: rateRev, + contractRevisions: [contractRev], + } + + return { + id: 'test-rate-234', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('02/01/2021'), + status: 'SUBMITTED', + stateCode: 'MN', + stateNumber: 2, + parentContractID: 'test-contract-234', + + revisions: [rateRev], + packageSubmissions: [rateSubmission], + + ...ratePartial, + } +} + +const mockUnlockedContract = ( + contractPartial?: Partial, + rateParitals?: Partial[] +): UnlockedContractType => { + const draftRates = rateParitals + ? rateParitals.map((r) => mockRate(r)) + : [mockRate()] + + return { + id: 'test-contract-123', + createdAt: new Date('01/01/2021'), + updatedAt: new Date('02/01/2021'), + status: 'UNLOCKED', + stateCode: 'MN', + stateNumber: 4, + + draftRevision: mockContractRev(), + draftRates, + + revisions: [], + packageSubmissions: [], + ...contractPartial, + } +} + const mockContractAndRatesFormData = ( submissionPartial?: Partial ): LockedHealthPlanFormDataType => { @@ -664,6 +738,7 @@ export { mockContractOnlyFormData, mockContractRev, mockContractAndRatesFormData, + mockUnlockedContract, mockUnlockedContractAndRatesFormData, mockUnlockedContractOnlyFormData, testEmailer, diff --git a/services/app-graphql/src/mutations/createContract.graphql b/services/app-graphql/src/mutations/createContract.graphql index b2d643fc50..5f5d4ad712 100644 --- a/services/app-graphql/src/mutations/createContract.graphql +++ b/services/app-graphql/src/mutations/createContract.graphql @@ -129,25 +129,6 @@ fragment rateRevisionFragmentForCreateContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForCreate - } - } } fragment contractFormDataFragmentForCreate on ContractFormData { @@ -213,6 +194,7 @@ fragment contractRevisionFragmentForCreate on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/mutations/submitContract.graphql b/services/app-graphql/src/mutations/submitContract.graphql index ea014c7c05..f796279ee0 100644 --- a/services/app-graphql/src/mutations/submitContract.graphql +++ b/services/app-graphql/src/mutations/submitContract.graphql @@ -129,25 +129,6 @@ fragment rateRevisionFragmentForSubmitContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForSubmit - } - } } fragment contractFormDataFragmentForSubmit on ContractFormData { @@ -213,6 +194,7 @@ fragment contractRevisionFragmentForSubmit on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/mutations/submitRate.graphql b/services/app-graphql/src/mutations/submitRate.graphql index d590f85d13..b003678678 100644 --- a/services/app-graphql/src/mutations/submitRate.graphql +++ b/services/app-graphql/src/mutations/submitRate.graphql @@ -76,71 +76,6 @@ mutation submitRate($input: SubmitRateInput!) { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - programIDs - populationCovered - submissionType - riskBasedContract - submissionDescription - stateContacts { - name, - titleRole - email - } - supportingDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractType - contractExecutionStatus - contractDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractDateStart - contractDateEnd - managedCareEntities - federalAuthorities - inLieuServicesAndSettings - modifiedBenefitsProvided - modifiedGeoAreaServed - modifiedMedicaidBeneficiaries - modifiedRiskSharingStrategy - modifiedIncentiveArrangements - modifiedWitholdAgreements - modifiedStateDirectedPayments - modifiedPassThroughPayments - modifiedPaymentsForMentalDiseaseInstitutions - modifiedMedicalLossRatioStandards - modifiedOtherFinancialPaymentIncentive - modifiedEnrollmentProcess - modifiedGrevienceAndAppeal - modifiedNetworkAdequacyStandards - modifiedLengthOfContract - modifiedNonRiskPaymentArrangements - } - } } } } diff --git a/services/app-graphql/src/mutations/unlockContract.graphql b/services/app-graphql/src/mutations/unlockContract.graphql index 79c0de90ee..5b8cd1bfcd 100644 --- a/services/app-graphql/src/mutations/unlockContract.graphql +++ b/services/app-graphql/src/mutations/unlockContract.graphql @@ -139,71 +139,6 @@ fragment rateRevisionFragmentForUnlockContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - programIDs - populationCovered - submissionType - riskBasedContract - submissionDescription - stateContacts { - name - titleRole - email - } - supportingDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractType - contractExecutionStatus - contractDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractDateStart - contractDateEnd - managedCareEntities - federalAuthorities - inLieuServicesAndSettings - modifiedBenefitsProvided - modifiedGeoAreaServed - modifiedMedicaidBeneficiaries - modifiedRiskSharingStrategy - modifiedIncentiveArrangements - modifiedWitholdAgreements - modifiedStateDirectedPayments - modifiedPassThroughPayments - modifiedPaymentsForMentalDiseaseInstitutions - modifiedMedicalLossRatioStandards - modifiedOtherFinancialPaymentIncentive - modifiedEnrollmentProcess - modifiedGrevienceAndAppeal - modifiedNetworkAdequacyStandards - modifiedLengthOfContract - modifiedNonRiskPaymentArrangements - } - } } fragment contractFormDataFragmentForUnlock on ContractFormData { @@ -267,6 +202,7 @@ fragment contractRevisionFragmentForUnlock on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/mutations/unlockRate.graphql b/services/app-graphql/src/mutations/unlockRate.graphql index f5eff15158..2817b61a7b 100644 --- a/services/app-graphql/src/mutations/unlockRate.graphql +++ b/services/app-graphql/src/mutations/unlockRate.graphql @@ -56,71 +56,6 @@ fragment rateRevisionFragmentForUnlock on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - programIDs - populationCovered - submissionType - riskBasedContract - submissionDescription - stateContacts { - name, - titleRole - email - } - supportingDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractType - contractExecutionStatus - contractDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractDateStart - contractDateEnd - managedCareEntities - federalAuthorities - inLieuServicesAndSettings - modifiedBenefitsProvided - modifiedGeoAreaServed - modifiedMedicaidBeneficiaries - modifiedRiskSharingStrategy - modifiedIncentiveArrangements - modifiedWitholdAgreements - modifiedStateDirectedPayments - modifiedPassThroughPayments - modifiedPaymentsForMentalDiseaseInstitutions - modifiedMedicalLossRatioStandards - modifiedOtherFinancialPaymentIncentive - modifiedEnrollmentProcess - modifiedGrevienceAndAppeal - modifiedNetworkAdequacyStandards - modifiedLengthOfContract - modifiedNonRiskPaymentArrangements - } - } } mutation unlockRate($input: UnlockRateInput!) { diff --git a/services/app-graphql/src/mutations/updateContractDraftRevision.graphql b/services/app-graphql/src/mutations/updateContractDraftRevision.graphql index e36e83eb38..7db43a56e7 100644 --- a/services/app-graphql/src/mutations/updateContractDraftRevision.graphql +++ b/services/app-graphql/src/mutations/updateContractDraftRevision.graphql @@ -129,25 +129,6 @@ fragment rateRevisionFragmentForUpdateContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForUpdate - } - } } fragment contractFormDataFragmentForUpdate on ContractFormData { @@ -213,6 +194,7 @@ fragment contractRevisionFragmentForUpdate on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/mutations/withdrawAndReplaceRedundantRate.graphql b/services/app-graphql/src/mutations/withdrawAndReplaceRedundantRate.graphql index f255efca53..0b405a37ce 100644 --- a/services/app-graphql/src/mutations/withdrawAndReplaceRedundantRate.graphql +++ b/services/app-graphql/src/mutations/withdrawAndReplaceRedundantRate.graphql @@ -129,25 +129,6 @@ fragment rateRevisionFragmentForReplaceRate on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForReplaceRate - } - } } fragment contractFormDataFragmentForReplaceRate on ContractFormData { @@ -213,6 +194,7 @@ fragment contractRevisionFragmentForReplaceRate on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/queries/fetchContract.graphql b/services/app-graphql/src/queries/fetchContract.graphql index 8f8b7a7312..5d43155cbf 100644 --- a/services/app-graphql/src/queries/fetchContract.graphql +++ b/services/app-graphql/src/queries/fetchContract.graphql @@ -129,25 +129,6 @@ fragment rateRevisionFragmentForFetchContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForFetch - } - } } fragment contractFormDataFragmentForFetch on ContractFormData { @@ -213,6 +194,7 @@ fragment contractRevisionFragmentForFetch on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/queries/fetchContractWithQuestions.graphql b/services/app-graphql/src/queries/fetchContractWithQuestions.graphql index bdfbaa58f0..3636ad3e27 100644 --- a/services/app-graphql/src/queries/fetchContractWithQuestions.graphql +++ b/services/app-graphql/src/queries/fetchContractWithQuestions.graphql @@ -150,25 +150,6 @@ fragment rateRevisionFragmentForFetchContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForFetch - } - } } fragment contractFormDataFragmentForFetch on ContractFormData { @@ -234,6 +215,7 @@ fragment contractRevisionFragmentForFetch on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/queries/fetchRate.graphql b/services/app-graphql/src/queries/fetchRate.graphql index 5b4dedd321..562b05815f 100644 --- a/services/app-graphql/src/queries/fetchRate.graphql +++ b/services/app-graphql/src/queries/fetchRate.graphql @@ -57,71 +57,6 @@ fragment rateRevisionFragmentForFetchRate on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - programIDs - populationCovered - submissionType - riskBasedContract - submissionDescription - stateContacts { - name, - titleRole - email - } - supportingDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractType - contractExecutionStatus - contractDocuments { - name - s3URL - sha256 - dateAdded - downloadURL - } - contractDateStart - contractDateEnd - managedCareEntities - federalAuthorities - inLieuServicesAndSettings - modifiedBenefitsProvided - modifiedGeoAreaServed - modifiedMedicaidBeneficiaries - modifiedRiskSharingStrategy - modifiedIncentiveArrangements - modifiedWitholdAgreements - modifiedStateDirectedPayments - modifiedPassThroughPayments - modifiedPaymentsForMentalDiseaseInstitutions - modifiedMedicalLossRatioStandards - modifiedOtherFinancialPaymentIncentive - modifiedEnrollmentProcess - modifiedGrevienceAndAppeal - modifiedNetworkAdequacyStandards - modifiedLengthOfContract - modifiedNonRiskPaymentArrangements - } - } } fragment rateFieldsForFetchRate on Rate { @@ -250,6 +185,7 @@ fragment contractRevisionFragmentForFetchRate on ContractRevision { id createdAt updatedAt + contractID contractName submitInfo { diff --git a/services/app-graphql/src/queries/indexContracts.graphql b/services/app-graphql/src/queries/indexContracts.graphql index 32898a93ba..25da31f1d0 100644 --- a/services/app-graphql/src/queries/indexContracts.graphql +++ b/services/app-graphql/src/queries/indexContracts.graphql @@ -132,25 +132,6 @@ fragment rateRevisionFragmentForIndexContract on RateRevision { packageStatus } } - contractRevisions { - id - contract { - id - stateCode - stateNumber - } - createdAt - updatedAt - submitInfo { - ...updateInformationFields - } - unlockInfo { - ...updateInformationFields - } - formData { - ...contractFormDataFragmentForIndex - } - } } fragment contractFormDataFragmentForIndex on ContractFormData { diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index adda36a640..7aeb6789fc 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1467,34 +1467,6 @@ type UpdateDraftContractRatesPayload { contract: Contract! } -type ContractOnRevisionType { - id: String! - "The two letter abbreviation for the state the contract covers" - stateCode: String! - """ - Maps a MC-Review contract to the record number - used in the MC-CRS system - """ - mccrsID: String - "Number of contracts a state has" - stateNumber: Int! -} - -type RelatedContractRevisions { - id: String! - contract: ContractOnRevisionType! - """ - Information about who, when, and why this revision was unlocked. - Will be blank on the initial revision. - """ - submitInfo: UpdateInformation - "Information on who, when, and why this revision was submitted." - unlockInfo: UpdateInformation - createdAt: DateTime! - updatedAt: DateTime! - formData: ContractFormData! -} - """ A rate revision represents a single submission for the rate and contains the full data from when the rate cert was submitted @@ -1514,8 +1486,6 @@ type RateRevision { submitInfo: UpdateInformation "The rate related form data that was inputed by the state" formData: RateFormData! - "Contract revisions related to the rate" - contractRevisions: [RelatedContractRevisions!]! } """ @@ -1569,6 +1539,8 @@ it contains the unlock and submit info. """ type ContractRevision { id: ID! + contractID: String! + createdAt: DateTime! updatedAt: DateTime! "submitInfo is the who/when/why for this revision being submitted. An Unlocked revision has no submitInfo" diff --git a/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx b/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx index 1bbb61a641..eb12ca83f2 100644 --- a/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx +++ b/services/app-web/src/components/Banner/UserAccountWarningBanner/UserAccountWarningBanner.tsx @@ -22,7 +22,7 @@ const UserAccountWarningBanner = ({ type: 'warn', extension: 'react-uswds', }) - }, [logAlertImpressionEvent,message]) + }, [logAlertImpressionEvent, message]) return (
diff --git a/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx b/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx index 7e711b50d4..b3ad253c4b 100644 --- a/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx +++ b/services/app-web/src/components/ErrorAlert/ErrorAlert.tsx @@ -28,7 +28,7 @@ export const ErrorAlert = ({ const showLink = appendLetUsKnow || !message // our default message includes the link const defaultMessage = "We're having trouble loading this page. Please refresh your browser and if you continue to experience an error," - const logErrorMessage = `${message ? extractText(message) : defaultMessage} email ${MAIL_TO_SUPPORT}` + const logErrorMessage = `${message ? extractText(message) : defaultMessage} email ${MAIL_TO_SUPPORT}` useEffect(() => { logAlertImpressionEvent({ @@ -37,7 +37,7 @@ export const ErrorAlert = ({ type: 'error', extension: 'react-uswds', }) - }, [logAlertImpressionEvent,logErrorMessage]) + }, [logAlertImpressionEvent, logErrorMessage]) return ( { it('can render rate details without errors', async () => { - const rateData = rateDataMock() + const rateData = rateWithHistoryMock() rateData.revisions[0].formData.deprecatedRateProgramIDs = ['123'] - await waitFor(() => { - renderWithProviders( - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - ], - }, - featureFlags: { - 'rate-edit-unlock': true, - }, - } - ) - }) + renderWithProviders( + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + ], + }, + featureFlags: { + 'rate-edit-unlock': true, + }, + } + ) // Wait for all the documents to be in the table await screen.findByText( rateData.revisions[0].formData.rateDocuments[0].name @@ -109,8 +108,13 @@ describe('SingleRateSummarySection', () => { }) // can delete the next test when linked rates flag is permanently on it('renders documents with linked submissions correctly for CMS users (legacy feature)', async () => { - const rateData = rateDataMock() - const parentContractRev = rateData.revisions[0].contractRevisions[0] + const rateData = rateWithHistoryMock() + const lastSubmission = rateData.packageSubmissions?.[0] + if (!lastSubmission) { + throw new Error('no sub') + } + + const parentContractRev = lastSubmission.contractRevisions[0] const rateDoc = rateData.revisions[0].formData.rateDocuments[0] const supportingDoc = rateData.revisions[0].formData.supportingDocuments[0] @@ -119,34 +123,27 @@ describe('SingleRateSummarySection', () => { const linkedSubmissionTwo = rateData.revisions[0].formData.packagesWithSharedRateCerts[1] - const contractPackageName = packageName( - parentContractRev.contract.stateCode, - parentContractRev.contract.stateNumber, - parentContractRev.formData.programIDs, - rateData.state.programs + const contractPackageName = parentContractRev.contractName + + renderWithProviders( + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + ], + }, + featureFlags: { 'rate-edit-unlock': true }, + } ) - await waitFor(() => { - renderWithProviders( - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - ], - }, - featureFlags: { 'rate-edit-unlock': true }, - } - ) - }) - expect( screen.getByRole('heading', { name: 'Rate documents' }) ).toBeInTheDocument() @@ -181,7 +178,7 @@ describe('SingleRateSummarySection', () => { }) ).toHaveAttribute( 'href', - `/submissions/${parentContractRev.contract.id}` + `/submissions/${parentContractRev.contractID}` ) // Expect rate certification document and linked submissions @@ -222,38 +219,35 @@ describe('SingleRateSummarySection', () => { ).toBeInTheDocument() }) it('renders rates linked to other contract actions correctly', async () => { - const rateData = rateDataMock() - const parentContractRev = rateData.revisions[0].contractRevisions[0] - - const contractPackageName = packageName( - parentContractRev.contract.stateCode, - parentContractRev.contract.stateNumber, - parentContractRev.formData.programIDs, - rateData.state.programs - ) + const rateData = rateWithHistoryMock() + const parentContractRev = + rateData.packageSubmissions?.[2].contractRevisions[0] + if (!parentContractRev) { + throw new Error('no parent') + } - await waitFor(() => { - renderWithProviders( - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - ], - }, - featureFlags: { - 'rate-edit-unlock': true, - }, - } - ) - }) + const contractPackageName = parentContractRev.contractName + + renderWithProviders( + , + { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + ], + }, + featureFlags: { + 'rate-edit-unlock': true, + }, + } + ) expect( screen.getByRole('heading', { name: 'Rate documents' }) @@ -276,19 +270,13 @@ describe('SingleRateSummarySection', () => { }) ).toHaveAttribute( 'href', - `/submissions/${parentContractRev.contract.id}` + `/submissions/${parentContractRev.contractID}` ) }) describe('Unlock rate', () => { it('renders the unlock button to CMS users when rate edit and unlock is enabled', async () => { - const rateData = rateDataMock( - { - rateType: undefined, - rateDateCertified: undefined, - } as unknown as Partial, - { status: 'UNLOCKED' } - ) + const rateData = rateWithHistoryMock() renderWithProviders( { it('renders unlock button that redirects to contract submission page when linked rates on but standalone rate edit and unlock is still disabled', async () => { let testLocation: Location // set up location to track URL changes - const rateData = rateDataMock() + const rateData = rateWithHistoryMock() renderWithProviders( { }) it('disables the unlock button for CMS users when rate already unlocked', async () => { - const rateData = rateDataMock( - { - rateType: undefined, - rateDateCertified: undefined, - } as unknown as Partial, - { status: 'UNLOCKED' } - ) + const rateData = rateUnlockedWithHistoryMock() renderWithProviders( { }) it('does not render the unlock button to state users', async () => { - const rateData = rateDataMock( - { - rateType: undefined, - rateDateCertified: undefined, - } as unknown as Partial, - { status: 'UNLOCKED' } - ) + const rateData = rateWithHistoryMock() renderWithProviders( { }) describe('Missing data error notifications', () => { - const draftRates = mockEmptyDraftContractAndRate().draftRates - - if (!draftRates) { - throw new Error('Unexpected error: draft rates is undefined') + const mockEmptyRateData = () => { + const emptyDraftRates = mockEmptyDraftContractAndRate().draftRates + if (!emptyDraftRates) { + throw new Error('Unexpected error: draft rates is undefined') + } + + const emptyRateFormData = emptyDraftRates[0].draftRevision?.formData + + if (!emptyRateFormData) { + throw new Error('no form data') + } + + const rateData = rateWithHistoryMock() + const sub = rateData.packageSubmissions?.[0] + if (!sub) { + throw new Error('no sub') + } + sub.rateRevision.formData = emptyRateFormData + + return rateData } - const emptyRateFormData = draftRates[0].draftRevision - ?.formData as RateFormData - - const mockEmptyRateData = () => - rateDataMock( - { - formData: { - ...emptyRateFormData, - rateType: 'AMENDMENT', - }, - }, - { status: 'UNLOCKED' } - ) - it('should not display missing field text to CMS users', async () => { const rateData = mockEmptyRateData() + renderWithProviders( { it('should display missing field text to state users', async () => { const rateData = mockEmptyRateData() + + if ( + !rateData.packageSubmissions || + rateData.packageSubmissions.length === 0 + ) { + throw new Error('no package subs') + } + + rateData.packageSubmissions[0].rateRevision.formData.rateType = + 'AMENDMENT' + renderWithProviders( { } const relatedSubmissions = ( - contractRevisions: RelatedContractRevisions[], - statePrograms: Program[] + contractRevisions: ContractRevision[] ): React.ReactElement => { return (
    {contractRevisions.map((contractRev) => ( -
  • +
  • - {packageName( - contractRev.contract.stateCode, - contractRev.contract.stateNumber, - contractRev.formData.programIDs, - statePrograms - )} + {contractRev.contractName}
  • ))} @@ -108,9 +101,20 @@ export const SingleRateSummarySection = ({ }): React.ReactElement | null => { const { loggedInUser } = useAuth() const navigate = useNavigate() - const rateRevision = rate.revisions[0] - const formData: RateFormData = rateRevision?.formData - const lastSubmittedDate = rate.revisions[0]?.submitInfo?.updatedAt ?? null + + const latestSubmission = rate.packageSubmissions?.[0] + if (!latestSubmission) { + // This is unusual and ugly, we try not to throw ever, but we can't early return here. + // of course if the array were required we would just silently throw if its empty + throw new Error( + 'programming error: should not have a summarized rate without a submission' + ) + } + + const rateRevision = latestSubmission.rateRevision + const formData: RateFormData = rateRevision.formData + const lastSubmittedDate = latestSubmission.submitInfo.updatedAt + const isRateAmendment = formData.rateType === 'AMENDMENT' const isUnlocked = rate.status === 'UNLOCKED' const explainMissingData = @@ -130,7 +134,7 @@ export const SingleRateSummarySection = ({ featureFlags.RATE_EDIT_UNLOCK.defaultValue ) - const linkedContracts = rateRevision?.contractRevisions + const linkedContracts = latestSubmission.contractRevisions // TODO BULK DOWNLOAD // needs to be wrap in a standalone hook @@ -403,10 +407,7 @@ export const SingleRateSummarySection = ({ diff --git a/services/app-web/src/contexts/AuthContext.tsx b/services/app-web/src/contexts/AuthContext.tsx index 967dadf14d..5bf9345426 100644 --- a/services/app-web/src/contexts/AuthContext.tsx +++ b/services/app-web/src/contexts/AuthContext.tsx @@ -97,7 +97,7 @@ function AuthProvider({ ) modalCountdownTimers.current = [] } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionIsExpiring]) // full dep array causes a loop, because we're resetting the dep in the useEffect const isAuthenticated = loggedInUser !== undefined diff --git a/services/app-web/src/contexts/TealiumContext.tsx b/services/app-web/src/contexts/TealiumContext.tsx index bbda9b84b5..b52e6d76a0 100644 --- a/services/app-web/src/contexts/TealiumContext.tsx +++ b/services/app-web/src/contexts/TealiumContext.tsx @@ -51,7 +51,7 @@ const TealiumProvider = ({ client, children }: TealiumProviderProps) => { // This effect should only fire on initial app load useEffect(() => { initializeTealium() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // This effect should only fire each time the url changes diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index e34928d2fb..a2fa45a8cd 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -121,9 +121,7 @@ const StateUserRoutes = ({ )} }> } /> }> } /> { routerProvider: { route: '/submissions/15/question-and-answers?submit=response', }, - } ) @@ -543,7 +542,6 @@ describe('QuestionResponse', () => { routerProvider: { route: '/submissions/15/question-and-answers', }, - } ) diff --git a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx index c442d7a6a4..03ec5aa0d5 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx @@ -59,9 +59,9 @@ export const UploadQuestions = () => { const { setFocusErrorSummaryHeading, errorSummaryHeadingRef } = useErrorSummary() - if (pkg.status === 'DRAFT') { - return - } + if (pkg.status === 'DRAFT') { + return + } const showFileUploadError = Boolean(shouldValidate && fileUploadError) const fileUploadErrorFocusKey = hasNoFiles ? 'questions-upload' diff --git a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx index a0d3760776..0114218435 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx @@ -63,9 +63,9 @@ export const UploadResponse = () => { } = useFileUpload(shouldValidate) const { setFocusErrorSummaryHeading, errorSummaryHeadingRef } = useErrorSummary() - if (pkg.status === 'DRAFT') { - return - } + if (pkg.status === 'DRAFT') { + return + } const showFileUploadError = Boolean(shouldValidate && fileUploadError) const fileUploadErrorFocusKey = hasNoFiles diff --git a/services/app-web/src/pages/RateSummary/RateSummary.test.tsx b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx index 66a0c2dc0e..3e3df2a6e8 100644 --- a/services/app-web/src/pages/RateSummary/RateSummary.test.tsx +++ b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx @@ -11,6 +11,7 @@ import { RoutesRecord } from '../../constants' import { Route, Routes } from 'react-router-dom' import { RateEdit } from '../RateEdit/RateEdit' import { dayjs } from '../../common-code/dateHelpers' +import { rateWithHistoryMock } from '../../testHelpers/apolloMocks/rateDataMock' // Wrap test component in some top level routes to allow getParams to be tested const wrapInRoutes = (children: React.ReactNode) => { @@ -59,6 +60,7 @@ describe('RateSummary', () => { fetchRateMockSuccess({ id: '1337', withdrawInfo: { + __typename: 'UpdateInformation', updatedAt: new Date('2024-01-01'), updatedBy: { email: 'admin@example.com', @@ -191,11 +193,11 @@ describe('RateSummary', () => { user: mockValidStateUser(), statusCode: 200, }), - fetchRateMockSuccess({ id: '1337' }), + fetchRateMockSuccess(rateWithHistoryMock()), ], }, routerProvider: { - route: '/rates/1337', + route: '/rates/r-01', }, featureFlags: { 'rate-edit-unlock': true }, }) @@ -220,6 +222,7 @@ describe('RateSummary', () => { fetchRateMockSuccess({ id: '1337', withdrawInfo: { + __typename: 'UpdateInformation', updatedAt: new Date('2024-01-01'), updatedBy: { email: 'admin@example.com', diff --git a/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss b/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss index 6a48fb3d80..e70f8501d5 100644 --- a/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss +++ b/services/app-web/src/pages/ReplaceRate/ReplaceRate.module.scss @@ -1,15 +1,15 @@ @use '../../styles/custom.scss' as custom; @use '../../styles/uswdsImports.scss' as uswds; -.background{ - width: 100%; - flex: 1 0 auto; - display: flex; - flex-direction: column; - background-color: none; - align-items: center +.background { + width: 100%; + flex: 1 0 auto; + display: flex; + flex-direction: column; + background-color: none; + align-items: center; } -.gridContainer{ +.gridContainer { @include custom.default-page-container; max-width: custom.$mcr-container-standard-width-fixed; flex-direction: column; diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx index 340d7aaf2b..ae8c74d0d7 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx @@ -20,6 +20,7 @@ import userEvent from '@testing-library/user-event' import { rateDataMock, rateRevisionDataMock, + draftRateDataMock, } from '../../../testHelpers/apolloMocks/rateDataMock' import { fetchDraftRateMockSuccess, @@ -809,6 +810,38 @@ describe('RateDetails', () => { }) it('displays dropdown menu if yes is selected and dropdown is clicked', async () => { + const testContract = { + ...mockContractWithLinkedRateDraft({ + draftRates: [ + draftRateDataMock( + { id: 'test-abc-124' }, + { + formData: { + ...rateRevisionDataMock().formData, + rateDocuments: [ + { + s3URL: 's3://bucketname/one-one/one-one.png', + name: 'one one', + sha256: 'fakeSha1', + }, + { + s3URL: 's3://bucketname/one-two/one-two.png', + name: 'one two', + sha256: 'fakeSha2', + }, + { + s3URL: 's3://bucketname/one-three/one-three.png', + name: 'one three', + sha256: 'fakeSha3', + }, + ], + }, + } + ), + ], + }), + } + const { user } = renderWithProviders( { indexRatesMockSuccess(), fetchCurrentUserMock({ statusCode: 200 }), fetchContractMockSuccess({ - contract: { - ...mockContractWithLinkedRateDraft({ - draftRates: [ - rateDataMock( - { - formData: { - ...rateRevisionDataMock() - .formData, - rateDocuments: [ - { - s3URL: 's3://bucketname/one-one/one-one.png', - name: 'one one', - sha256: 'fakeSha1', - }, - { - s3URL: 's3://bucketname/one-two/one-two.png', - name: 'one two', - sha256: 'fakeSha2', - }, - { - s3URL: 's3://bucketname/one-three/one-three.png', - name: 'one three', - sha256: 'fakeSha3', - }, - ], - }, - }, - { id: 'test-abc-123' } - ), - ], - }), - }, + contract: testContract, }), indexRatesMockSuccess(), ], diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.test.tsx index 18ecba0b3d..60e0006b9c 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.test.tsx @@ -333,6 +333,7 @@ describe('ContractDetailsSummarySection', () => { submitInfo: undefined, unlockInfo: undefined, id: '123', + contractID: 'test-abc-123', createdAt: new Date(), updatedAt: new Date(), contractName: 'MCR-0005-alvhalfhdsalfdd', diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx index 800f64a07c..23ee46dbe4 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ContractDetailsSummarySection.tsx @@ -184,7 +184,7 @@ export const ContractDetailsSummarySection = ({ const lastSubmittedDate = isPreviousSubmission ? getPackageSubmissionAtIndex(contract, lastSubmittedIndex)?.submitInfo .updatedAt - : getLastContractSubmission(contract)?.submitInfo.updatedAt ?? null + : (getLastContractSubmission(contract)?.submitInfo.updatedAt ?? null) return ( { const draftContract = mockContractPackageDraft() @@ -41,7 +42,6 @@ describe('RateDetailsSummarySection', () => { rateID: '5678', createdAt: new Date('01/01/2021'), updatedAt: new Date('01/01/2021'), - contractRevisions: [], formData: { rateType: 'NEW', rateCapitationType: 'RATE_CELL', @@ -99,7 +99,6 @@ describe('RateDetailsSummarySection', () => { rateID: '5678', createdAt: new Date('01/01/2021'), updatedAt: new Date('01/01/2021'), - contractRevisions: [], formData: { rateType: 'AMENDMENT', rateCapitationType: 'RATE_CELL', @@ -343,25 +342,12 @@ describe('RateDetailsSummarySection', () => { }) it('does not render replace rate button for rates linked to another contract', async () => { - const contract = mockContractWithLinkedRateSubmitted() - const contractB = mockContractPackageSubmittedWithRevisions() + const { c1 } = submittedLinkedRatesScenarioMock() const rateRevs: RateRevisionWithIsLinked[] = - contract.packageSubmissions[0].rateRevisions.map((rev) => { + c1.packageSubmissions[0].rateRevisions.map((rev) => { const newRev: RateRevisionWithIsLinked = { ...rev, - contractRevisions: [ - ...rev.contractRevisions, - // Add a second contract revision to simulate this rate linked to another contract - { - ...contractB.packageSubmissions[0].contractRevision, - __typename: 'RelatedContractRevisions', - contract: { - ...contractB, - __typename: 'ContractOnRevisionType', - }, - }, - ], - isLinked: false, + isLinked: true, } return newRev }) @@ -369,7 +355,7 @@ describe('RateDetailsSummarySection', () => { await waitFor(() => { renderWithProviders( 1 + const isLinkedTo = rateRev.isLinked /** Rate programs switched in summer 2024. We still show deprecated program field values when diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.test.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.test.tsx index f383892e15..3cd3e8db9a 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.test.tsx @@ -8,7 +8,10 @@ import { } from '../../../testHelpers/apolloMocks' import { Route, Routes } from 'react-router-dom' import { RoutesRecord } from '../../../constants' -import { mockContractPackageDraft, mockContractPackageUnlocked } from '../../../testHelpers/apolloMocks/contractPackageDataMock' +import { + mockContractPackageDraft, + mockContractPackageUnlocked, +} from '../../../testHelpers/apolloMocks/contractPackageDataMock' describe('ReviewSubmit', () => { it('renders without errors', async () => { @@ -135,7 +138,10 @@ describe('ReviewSubmit', () => { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), fetchContractMockSuccess({ - contract: { ...mockContractPackageDraft(),id: 'test-abc-123' }, + contract: { + ...mockContractPackageDraft(), + id: 'test-abc-123', + }, }), ], }, @@ -219,7 +225,10 @@ describe('ReviewSubmit', () => { mocks: [ fetchCurrentUserMock({ statusCode: 200 }), fetchContractMockSuccess({ - contract: { ...mockContractPackageDraft(), id: 'test-abc-123' }, + contract: { + ...mockContractPackageDraft(), + id: 'test-abc-123', + }, }), ], }, diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx index 5813143c05..02f9d0ec9f 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx @@ -310,7 +310,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -353,7 +352,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -396,7 +394,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15', }, - } ) @@ -431,7 +428,6 @@ describe('SubmissionSideNav', () => { ], }, routerProvider: { route: '/submissions/404' }, - } ) @@ -470,7 +466,6 @@ describe('SubmissionSideNav', () => { routerProvider: { route: '/submissions/15/question-and-answers', }, - } ) diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx index 59bee1725a..45ae4a77ad 100644 --- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx +++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx @@ -1,6 +1,5 @@ import { GridContainer, - Icon, Link, ModalRef, ModalToggleButton, @@ -15,7 +14,6 @@ import { SubmissionUnlockedBanner, SubmissionUpdatedBanner, DocumentWarningBanner, - NavLinkWithLogging, LinkWithLogging, } from '../../components' import { Loading } from '../../components' @@ -202,52 +200,50 @@ export const SubmissionSummary = (): React.ReactElement => { )} - - {contract.mccrsID && ( - - MC-CRS record number: - - {contract.mccrsID} - - - )} - - {editOrAddMCCRSID} - -
- ) : undefined - } - contract={contract} - submissionName={name} - headerChildComponent={ - hasCMSPermissions ? ( - - ) : undefined - } - statePrograms={statePrograms} - initiallySubmittedAt={contract.initiallySubmittedAt} - isStateUser={isStateUser} - explainMissingData={explainMissingData} - /> + + {contract.mccrsID && ( + + MC-CRS record number: + + {contract.mccrsID} + + + )} + + {editOrAddMCCRSID} + + + ) : undefined + } + contract={contract} + submissionName={name} + headerChildComponent={ + hasCMSPermissions ? ( + + ) : undefined + } + statePrograms={statePrograms} + initiallySubmittedAt={contract.initiallySubmittedAt} + isStateUser={isStateUser} + explainMissingData={explainMissingData} + /> { createdAt: new Date('01/01/2024'), updatedAt: new Date('12/31/2024'), id: `123${name}`, + contractID: `contract-123-${name}`, submitInfo: { updatedAt: new Date(`2024-02-17T03:2${name}:00`), updatedBy: { @@ -34,13 +35,15 @@ function mockContractRevision(name?: string, partial?: Partial s3URL: `s3://bucketname/key/contractsupporting${name}1`, sha256: 'fakesha', name: `contractSupporting${name}1`, - dateAdded: new Date('01/15/2024') + dateAdded: new Date('01/15/2024'), + downloadURL: s3DlUrl, }, { s3URL: `s3://bucketname/key/contractsupporting${name}2`, sha256: 'fakesha', name: `contractSupporting${name}2`, - dateAdded: new Date('01/13/2024') + dateAdded: new Date('01/13/2024'), + downloadURL: s3DlUrl, }, ], stateContacts: [], @@ -51,7 +54,8 @@ function mockContractRevision(name?: string, partial?: Partial s3URL: `s3://bucketname/key/contract${name}`, sha256: 'fakesha', name: `contract${name}`, - dateAdded: new Date('2024-02-17') + dateAdded: new Date('2024-02-17'), + downloadURL: s3DlUrl, }, ], contractDateStart: new Date(), @@ -110,7 +114,6 @@ function mockRateRevision(name?: string, partial?: Partial): RateR }, updatedReason: 'contract unlock' }, - contractRevisions: [], formData: { __typename: 'RateFormData', rateType: 'AMENDMENT', @@ -192,6 +195,7 @@ function mockContractPackageDraft( submitInfo: undefined, unlockInfo: undefined, id: '123', + contractID: 'test-abc-123', createdAt: new Date('01/01/2023'), updatedAt: new Date('11/01/2023'), contractName: 'MCR-0005-alvhalfhdsalfee', @@ -212,7 +216,6 @@ function mockContractPackageDraft( draftRevision: { id: '123', rateID: '456', - contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), formData: { @@ -296,6 +299,7 @@ function mockContractWithLinkedRateDraft( submitInfo: undefined, unlockInfo: undefined, id: '123', + contractID: 'test-abc-123', createdAt: new Date('01/01/2023'), updatedAt: new Date('11/01/2023'), contractName: 'MCR-0005-alvhalfhdsalfss', @@ -363,7 +367,6 @@ function mockContractWithLinkedRateDraft( draftRevision: { id: '123', rateID: 'rate-123', - contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), submitInfo: { @@ -428,7 +431,6 @@ function mockContractWithLinkedRateDraft( revisions: [{ id: '456', rateID: 'rate-123', - contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), submitInfo: undefined, @@ -512,6 +514,7 @@ function mockContractWithLinkedRateSubmitted( createdAt: new Date('01/01/2024'), updatedAt: new Date('12/31/2024'), id: '123', + contractID: 'test-abc-123', submitInfo: { updatedAt: new Date(), updatedBy: { @@ -583,85 +586,6 @@ function mockContractWithLinkedRateSubmitted( { id: '123', rateID: 'rate-123', - contractRevisions: [ - { - contract: { - id: 'test-abc-123', - stateCode: 'MN', - stateNumber: 5, - mccrsID: undefined, - }, - createdAt: new Date('01/01/2024'), - updatedAt: new Date('12/31/2024'), - id: '123', - submitInfo: { - updatedAt: new Date(), - updatedBy: { - email: 'example@state.com', - role: 'STATE_USER', - givenName: 'John', - familyName: 'Vila' - }, - updatedReason: 'contract submit' - }, - unlockInfo: undefined, - formData: { - programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_AND_RATES', - riskBasedContract: true, - submissionDescription: 'A real submission', - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/contractsupporting1', - sha256: 'fakesha', - name: 'contractSupporting1', - dateAdded: new Date('01/15/2024') - }, - { - s3URL: 's3://bucketname/key/contractSupporting2', - sha256: 'fakesha', - name: 'contractSupporting2', - dateAdded: new Date('01/13/2024') - }, - ], - stateContacts: [], - contractType: 'AMENDMENT', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [ - { - s3URL: 's3://bucketname/key/contract', - sha256: 'fakesha', - name: 'contract', - dateAdded: new Date('01/01/2024') - }, - ], - contractDateStart: new Date(), - contractDateEnd: new Date(), - managedCareEntities: ['MCO'], - federalAuthorities: ['STATE_PLAN'], - inLieuServicesAndSettings: true, - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: true, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: false, - modifiedWitholdAgreements: false, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: true, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: true, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: true, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: true, - modifiedLengthOfContract: false, - modifiedNonRiskPaymentArrangements: true, - statutoryRegulatoryAttestation: true, - statutoryRegulatoryAttestationDescription: "everything meets regulatory attestation" - } - } - ], createdAt: new Date('12/18/2023'), updatedAt: new Date('12/18/2023'), formData: { @@ -741,6 +665,7 @@ function mockContractPackageSubmitted( createdAt: new Date('01/01/2024'), updatedAt: '2024-12-18T16:54:39.173Z', id: '123', + contractID: 'test-abc-123', submitInfo: { updatedAt: new Date(), updatedBy: { @@ -817,89 +742,6 @@ function mockContractPackageSubmitted( rateID: '123', createdAt: new Date('01/01/2023'), updatedAt: new Date('01/01/2023'), - contractRevisions: [ - { - contract: { - id: 'test-abc-123', - stateCode: 'MN', - stateNumber: 5, - mccrsID: undefined, - }, - createdAt: new Date('01/01/2024'), - updatedAt: '2024-12-18T16:54:39.173Z', - id: '123', - submitInfo: { - updatedAt: new Date(), - updatedBy: { - email: 'example@state.com', - role: 'STATE_USER', - givenName: 'John', - familyName: 'Vila' - }, - updatedReason: 'contract submit' - }, - unlockInfo: undefined, - formData: { - programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_AND_RATES', - riskBasedContract: true, - submissionDescription: 'A real submission', - supportingDocuments: [ - { - s3URL: 's3://bucketname/key/contractsupporting1', - sha256: 'fakesha', - name: 'contractSupporting1', - dateAdded: new Date('01/15/2024'), - downloadURL: s3DlUrl - }, - { - s3URL: 's3://bucketname/key/contractSupporting2', - sha256: 'fakesha', - name: 'contractSupporting2', - dateAdded: new Date('01/13/2024'), - downloadURL: s3DlUrl - }, - ], - stateContacts: [], - contractType: 'AMENDMENT', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [ - { - s3URL: 's3://bucketname/key/contract', - sha256: 'fakesha', - name: 'contract', - dateAdded: new Date('01/01/2024'), - downloadURL: s3DlUrl - }, - ], - contractDateStart: new Date(), - contractDateEnd: new Date(), - managedCareEntities: ['MCO'], - federalAuthorities: ['STATE_PLAN'], - inLieuServicesAndSettings: true, - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: true, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: false, - modifiedWitholdAgreements: false, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: true, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: true, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: true, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: true, - modifiedLengthOfContract: false, - modifiedNonRiskPaymentArrangements: true, - statutoryRegulatoryAttestation: true, - statutoryRegulatoryAttestationDescription: "everything meets regulatory attestation" - } - } - ], - rate: undefined, formData: { rateCertificationName:'rate cert', rateType: 'AMENDMENT', @@ -1147,6 +989,7 @@ function mockContractPackageWithDifferentProgramsInRevisions (): Contract { "createdAt": "2024-05-07T19:49:36.173Z", "updatedAt": "2024-05-08T17:42:52.696Z", "contractName": "MCR-FL-0221-DENTAL", + "contractID": "e670adsfdfadsfc", "submitInfo": { "updatedAt": "2024-05-08T17:42:52.696Z", "updatedBy": { @@ -1240,6 +1083,7 @@ function mockContractPackageWithDifferentProgramsInRevisions (): Contract { "createdAt": "2024-05-07T19:49:36.173Z", "updatedAt": "2024-05-08T17:42:52.696Z", "contractName": "MCR-FL-0221-DENTAL", + "contractID": "e670adsfdfadsfc", "submitInfo": { "updatedAt": "2024-05-08T17:42:52.696Z", "updatedBy": { @@ -1349,6 +1193,7 @@ function mockContractPackageWithDifferentProgramsInRevisions (): Contract { "createdAt": "2024-05-07T19:44:53.732Z", "updatedAt": "2024-05-07T19:46:19.377Z", "contractName": "MCR-FL-0221-PCCME", + "contractID": "e670adsfdfadsfc", "submitInfo": { "updatedAt": "2024-05-07T19:46:19.376Z", "updatedBy": { @@ -1424,6 +1269,7 @@ function mockContractPackageWithDifferentProgramsInRevisions (): Contract { "createdAt": "2024-05-07T19:44:53.732Z", "updatedAt": "2024-05-07T19:46:19.377Z", "contractName": "MCR-FL-0221-PCCME", + "contractID": "e670adsfdfadsfc", "submitInfo": { "updatedAt": "2024-05-07T19:46:19.376Z", "updatedBy": { @@ -1528,6 +1374,7 @@ function mockContractPackageUnlocked( updatedReason: 'unlocked for a test', }, id: '123', + contractID: 'test-abc-123', createdAt: new Date(), updatedAt: new Date(), contractName: 'MCR-MN-0005-SNBC', @@ -1552,7 +1399,8 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'one two', - dateAdded: new Date('02/02/2023') + dateAdded: new Date('02/02/2023'), + downloadURL: s3DlUrl, }, ], contractDateStart: new Date('02/02/2023'), @@ -1595,7 +1443,6 @@ function mockContractPackageUnlocked( draftRevision: { id: '123', rateID: '456', - contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), unlockInfo: { @@ -1616,7 +1463,8 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate', - dateAdded: new Date('03/02/2023') + dateAdded: new Date('03/02/2023'), + downloadURL: s3DlUrl, }, ], supportingDocuments: [], @@ -1688,6 +1536,7 @@ function mockContractPackageUnlocked( updatedReason: 'unlocked for a test' }, id: '123', + contractID: 'test-abc-123', formData: { programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], populationCovered: 'MEDICAID', @@ -1703,7 +1552,8 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - dateAdded: new Date() + dateAdded: new Date(), + downloadURL: s3DlUrl, }, ], contractDateStart: new Date('01/01/2023'), @@ -1757,6 +1607,7 @@ function mockContractPackageUnlocked( updatedReason: 'unlocked' }, id: '123', + contractID: 'test-abc-123', formData: { programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], populationCovered: 'MEDICAID', @@ -1772,7 +1623,8 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - dateAdded: new Date() + dateAdded: new Date(), + downloadURL: s3DlUrl, }, ], contractDateStart: new Date('01/01/2023'), @@ -1817,7 +1669,6 @@ function mockContractPackageUnlocked( }, updatedReason: 'initial submission' }, - contractRevisions: [], formData: { rateType: 'AMENDMENT', rateCapitationType: 'RATE_CELL', @@ -1826,7 +1677,8 @@ function mockContractPackageUnlocked( s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate', - dateAdded: new Date() + dateAdded: new Date(), + downloadURL: s3DlUrl, }, ], supportingDocuments: [], @@ -1878,13 +1730,15 @@ function mockContractFormData( partial?: Partial): ContractFor s3URL: 's3://bucketname/key/contractsupporting1', sha256: 'fakesha', name: 'contractSupporting1', - dateAdded: new Date('01/15/2024') + dateAdded: new Date('01/15/2024'), + downloadURL: s3DlUrl, }, { s3URL: 's3://bucketname/key/contractSupporting2', sha256: 'fakesha', name: 'contractSupporting2', - dateAdded: new Date('01/13/2024') + dateAdded: new Date('01/13/2024'), + downloadURL: s3DlUrl, }, ], stateContacts: [ @@ -1901,7 +1755,8 @@ function mockContractFormData( partial?: Partial): ContractFor s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'contract document', - dateAdded: new Date('01/01/2024') + dateAdded: new Date('01/01/2024'), + downloadURL: s3DlUrl, }, ], contractDateStart: new Date('01/01/2023'), @@ -1956,13 +1811,15 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( s3URL: 's3://bucketname/key/contractsupporting1', sha256: 'fakesha', name: 'contractSupporting1', - dateAdded: new Date('01/15/2024') + dateAdded: new Date('01/15/2024'), + downloadURL: s3DlUrl, }, { s3URL: 's3://bucketname/key/contractSupporting2', sha256: 'fakesha', name: 'contractSupporting2', - dateAdded: new Date('01/13/2024') + dateAdded: new Date('01/13/2024'), + downloadURL: s3DlUrl, }, ], stateContacts: [], @@ -1973,7 +1830,8 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'contract document', - dateAdded: new Date('01/01/2024') + dateAdded: new Date('01/01/2024'), + downloadURL: s3DlUrl, }, ], contractDateStart:undefined, @@ -2015,7 +1873,6 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( draftRevision: { id: '123', rateID: '456', - contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), formData: { @@ -2026,7 +1883,8 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate certification', - dateAdded: new Date('01/13/2024') + dateAdded: new Date('01/13/2024'), + downloadURL: s3DlUrl, }, ], supportingDocuments: [ @@ -2034,13 +1892,15 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( s3URL: 's3://bucketname/key/ratesupporting1', sha256: 'fakesha', name: 'rateSupporting1', - dateAdded: new Date('01/15/2024') + dateAdded: new Date('01/15/2024'), + downloadURL: s3DlUrl, }, { s3URL: 's3://bucketname/key/rateSupporting2', sha256: 'fakesha', name: 'rateSupporting2', - dateAdded: new Date('01/13/2024') + dateAdded: new Date('01/13/2024'), + downloadURL: s3DlUrl, }, ], rateDateStart: undefined, @@ -2080,6 +1940,7 @@ const mockEmptyDraftContractAndRate = (): Contract => mockContractPackageDraft( ) export { + mockContractRevision, mockContractPackageDraft, mockContractPackageSubmitted, mockContractWithLinkedRateSubmitted, @@ -2089,6 +1950,5 @@ export { mockContractPackageSubmittedWithRevisions, mockContractPackageWithDifferentProgramsInRevisions, mockEmptyDraftContractAndRate, - mockContractRevision, mockRateRevision } diff --git a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts index 3aba8f5aa6..b6e35cd924 100644 --- a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts @@ -1,90 +1,19 @@ import { + Contract, + ContractPackageSubmission, + ContractRevision, Rate, + RatePackageSubmission, RateRevision, - RelatedContractRevisions, + UpdateInformation, } from '../../gen/gqlClient' import { s3DlUrl } from './documentDataMock' import { mockMNState } from './stateMock' import { v4 as uuidv4 } from 'uuid' +import { updateInfoMock } from './updateInfoMocks' +import { mockContractRevision } from './contractPackageDataMock' -const contractRevisionOnRateDataMock = ( - data?: Partial -): RelatedContractRevisions => ({ - __typename: 'RelatedContractRevisions', - id: uuidv4(), - contract: { - __typename: 'ContractOnRevisionType', - id: uuidv4(), - stateCode: 'MN', - stateNumber: 3, - }, - createdAt: '2023-10-16T18:52:16.295Z', - updatedAt: '2023-10-16T19:02:26.795Z', - submitInfo: { - __typename: 'UpdateInformation', - updatedAt: '2023-10-16T19:02:26.795Z', - updatedBy: { - email: 'aang@example.com', - role: 'STATE_USER', - familyName: 'Airman', - givenName: 'Aang' - }, - updatedReason: 'Initial submission', - }, - unlockInfo: null, - formData: { - __typename: 'ContractFormData', - programIDs: ['d95394e5-44d1-45df-8151-1cc1ee66f100'], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_AND_RATES', - riskBasedContract: false, - submissionDescription: 'description of contract and rates submission', - stateContacts: [ - { - __typename: 'StateContact', - name: 'Name', - titleRole: null, - email: 'example@example.com', - }, - ], - supportingDocuments: [], - contractType: 'BASE', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [ - { - __typename: 'GenericDocument', - name: 'contract-document.pdf', - s3URL: 's3://bucketname/key/contract-document', - downloadURL: s3DlUrl, - sha256: 'fakeSha', - dateAdded: new Date(), - }, - ], - contractDateStart: '2024-04-01', - contractDateEnd: '2025-03-31', - managedCareEntities: ['MCO'], - federalAuthorities: ['STATE_PLAN'], - inLieuServicesAndSettings: false, - modifiedBenefitsProvided: null, - modifiedGeoAreaServed: null, - modifiedMedicaidBeneficiaries: null, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: true, - modifiedWitholdAgreements: false, - modifiedStateDirectedPayments: false, - modifiedPassThroughPayments: false, - modifiedPaymentsForMentalDiseaseInstitutions: true, - modifiedMedicalLossRatioStandards: null, - modifiedOtherFinancialPaymentIncentive: null, - modifiedEnrollmentProcess: null, - modifiedGrevienceAndAppeal: null, - modifiedNetworkAdequacyStandards: null, - modifiedLengthOfContract: null, - modifiedNonRiskPaymentArrangements: true, - }, - ...data, -}) const rateRevisionDataMock = (data?: Partial): RateRevision => { return { id: data?.id ?? uuidv4(), @@ -174,7 +103,6 @@ const rateRevisionDataMock = (data?: Partial): RateRevision => { ], __typename: 'RateFormData', }, - contractRevisions: [contractRevisionOnRateDataMock()], __typename: 'RateRevision', ...data, } @@ -204,12 +132,56 @@ const draftRateDataMock = ( } } +function rateSubmissionPackageMock( + submitInfo: UpdateInformation, + rateRev: RateRevision, + contractRevs: ContractRevision[], +): RatePackageSubmission { + return { + __typename: 'RatePackageSubmission', + cause: 'CONTRACT_SUBMISSION', + submitInfo: submitInfo, + submittedRevisions: [rateRev, ...contractRevs], + rateRevision: rateRev, + contractRevisions: contractRevs, + } +} + +function contractSubmissionPackageMock( + submitInfo: UpdateInformation, + contractRev: ContractRevision, + rateRevs: RateRevision[], +): ContractPackageSubmission { + return { + __typename: 'ContractPackageSubmission', + cause: 'CONTRACT_SUBMISSION', + submitInfo: submitInfo, + submittedRevisions: [contractRev, ...rateRevs], + contractRevision: contractRev, + rateRevisions: rateRevs, + } +} + const rateDataMock = ( revision?: Partial, rate?: Partial ): Rate => { const rateID = rate?.id ?? uuidv4() - return { + + const { r1 } = submittedLinkedRatesScenarioMock() + const latestSub = r1.packageSubmissions?.[0] + if (!latestSub) { + throw new Error('Bad package submission') + } + + const latestRev = latestSub.rateRevision + const modifiedRev: RateRevision = { + ...latestRev, + ...revision + } + + const finalRate: Rate = { + ...r1, __typename: 'Rate', createdAt: '2023-10-16T19:01:21.389Z', updatedAt: '2023-10-16T19:01:21.389Z', @@ -220,63 +192,151 @@ const rateDataMock = ( initiallySubmittedAt: '2023-10-16', draftRevision: null, parentContractID: 'foo-bar', - ...rate, id: rateID, + ...rate, + } + + finalRate.revisions[0] = modifiedRev + latestSub.rateRevision = modifiedRev + + return finalRate +} + +// n. b. at time of this writing we don't return draftContracts on Rates yet, so this is simpler than the setup +// makes it seem. +function rateUnlockedWithHistoryMock(): Rate { + + const { r1 } = submittedLinkedRatesScenarioMock() + + const draftRevision = rateRevisionDataMock({ + id: 'rr-03', + submitInfo: null, + }) + + r1.status = 'UNLOCKED' + r1.draftRevision = draftRevision + + return r1 + +} + +function rateWithHistoryMock(): Rate { + const { r1 } = submittedLinkedRatesScenarioMock() + return r1 +} + + +function submittedLinkedRatesScenarioMock(): { + r1: Rate, + c1: Contract, + c2: Contract, +} { + const c1r1sub1 = updateInfoMock(new Date(2024, 1, 1), 'Initial Submission') + const r1r01 = rateRevisionDataMock({ + id: 'rr-01', + submitInfo: c1r1sub1, + }) + const c1r01 = mockContractRevision('1', { + contractID: 'c-01', + submitInfo: c1r1sub1, + contractName: 'MCR-MN-0005-SNBC', + }) + + const r1r1pkg = rateSubmissionPackageMock(c1r1sub1, r1r01, [c1r01]) + const c1r1pkg = contractSubmissionPackageMock(c1r1sub1, c1r01, [r1r01]) + + + const c2r1sub1 = updateInfoMock(new Date(2024, 1, 2), 'Initial Submission') + const c2r1 = mockContractRevision('2', { + contractID: 'c-02', + submitInfo: c2r1sub1, + contractName: 'MCR-MN-0006-SNBC', + }) + + const r1r1c2pkg = rateSubmissionPackageMock(c2r1sub1, r1r01, [c1r01, c2r1]) + const c2r1r1pkg = contractSubmissionPackageMock(c2r1sub1, c2r1, [r1r01]) + + const r1c2sub = updateInfoMock(new Date(2024, 1, 3), 'Rate Submission') + const r1r2 = rateRevisionDataMock({ + id: 'rr-02', + submitInfo: r1c2sub, + }) + + const r1r2c1c2pkg = rateSubmissionPackageMock(r1c2sub, r1r2, [c1r01, c2r1]) + const c1r1r2pkg = contractSubmissionPackageMock(r1c2sub, c1r01, [r1r2]) + const c2r1r2pkg = contractSubmissionPackageMock(r1c2sub, c2r1, [r1r2]) + + const c1: Contract = { + __typename: 'Contract', + initiallySubmittedAt: undefined, + status: 'SUBMITTED', + createdAt: new Date(2024, 1, 1), + updatedAt: new Date(), + id: 'c-01', + stateCode: 'MN', + state: mockMNState(), + stateNumber: 5, + mccrsID: undefined, + packageSubmissions: [ + c1r1r2pkg, + c1r1pkg, + ] + } + + const c2: Contract = { + __typename: 'Contract', + initiallySubmittedAt: undefined, + status: 'SUBMITTED', + createdAt: new Date(2024, 1, 1), + updatedAt: new Date(), + id: 'c-02', + stateCode: 'MN', + state: mockMNState(), + stateNumber: 5, + mccrsID: undefined, + packageSubmissions: [ + c2r1r2pkg, + c2r1r1pkg, + ] + } + + const r1: Rate = { + __typename: 'Rate', + createdAt: '2023-10-16T19:01:21.389Z', + updatedAt: '2023-10-16T19:01:21.389Z', + stateCode: 'MN', + stateNumber: 10, + state: mockMNState(), + status: 'RESUBMITTED', + initiallySubmittedAt: '2023-10-16', + draftRevision: null, + parentContractID: 'c-01', + id: 'r-01', + withdrawInfo: null, revisions: [ - rateRevisionDataMock({ - unlockInfo: { - __typename: 'UpdateInformation', - updatedAt: '2023-10-16T19:05:26.585Z', - updatedBy: { - email: 'zuko@example.com', - role: 'CMS_USER', - familyName: 'Hotman', - givenName: 'Zuko' - }, - updatedReason: 'Unlock', - }, - submitInfo: { - __typename: 'UpdateInformation', - updatedAt: '2023-10-16T19:06:20.581Z', - updatedBy: { - email: 'aang@example.com', - role: 'STATE_USER', - familyName: 'Airman', - givenName: 'Aang' - }, - updatedReason: 'Resubmit', - }, - contractRevisions: [ - contractRevisionOnRateDataMock({ - submitInfo: { - __typename: 'UpdateInformation', - updatedAt: '2023-10-16T19:06:20.643Z', - updatedBy: { - email: 'aang@example.com', - role: 'STATE_USER', - familyName: 'Airman', - givenName: 'Aang' - }, - updatedReason: 'Resubmit', - }, - unlockInfo: { - __typename: 'UpdateInformation', - updatedAt: '2023-10-16T19:05:26.660Z', - updatedBy: { - email: 'zuko@example.com', - role: 'CMS_USER', - familyName: 'Hotman', - givenName: 'Zuko' - }, - updatedReason: 'Unlock', - }, - }), - ], - ...revision, - }), - rateRevisionDataMock(), + r1r2, + r1r01, ], + packageSubmissions: [ + r1r2c1c2pkg, + r1r1c2pkg, + r1r1pkg, + ] + } + + return { + r1, + c1, + c2, } + } -export { rateDataMock, rateRevisionDataMock, draftRateDataMock } +export { + rateDataMock, + rateRevisionDataMock, + draftRateDataMock, + rateWithHistoryMock, + rateUnlockedWithHistoryMock, + submittedLinkedRatesScenarioMock, +} diff --git a/services/app-web/src/testHelpers/apolloMocks/updateInfoMocks.ts b/services/app-web/src/testHelpers/apolloMocks/updateInfoMocks.ts new file mode 100644 index 0000000000..abca676f51 --- /dev/null +++ b/services/app-web/src/testHelpers/apolloMocks/updateInfoMocks.ts @@ -0,0 +1,24 @@ +import { UpdateInformation } from "../../gen/gqlClient"; + +function updateInfoMock( + updatedAt?: Date, + updatedReason?: string, +): UpdateInformation { + const upAt = updatedAt ? updatedAt.toISOString() : '2023-01-01T16:54:39.173Z' + return { + __typename: 'UpdateInformation', + updatedAt: upAt, + updatedBy: { + __typename: 'UpdatedBy', + givenName: 'Aang', + familyName: 'Hotman', + role: 'STATE_USER', + email: 'example@state.com', + }, + updatedReason: updatedReason || 'initial submission' + } +} + +export { + updateInfoMock +}