From cecad9671e683b3a8357c3d35f3ae9eace970ecd Mon Sep 17 00:00:00 2001 From: Jason Lin <98117700+JasonLin0991@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:16:14 -0400 Subject: [PATCH] MCR-4450: Update rate resolver to return questions field (#2804) * Add fetchRateWithQuestions query * Update helpers to work with both types of questions. * Fix import in indexContracts test * Add questions field to rateResolver * Use helper function to convert question to payload * Update services/app-api/src/resolvers/rate/fetchRate.test.ts * Update test. --- .../src/domain-models/QuestionsType.ts | 22 +- .../app-api/src/domain-models/UserType.ts | 12 +- services/app-api/src/domain-models/index.ts | 1 + .../src/postgres/questionResponse/index.ts | 1 + .../questionResponse/questionHelpers.ts | 97 +++--- .../src/resolvers/configureResolvers.ts | 2 +- .../resolvers/contract/contractResolver.ts | 46 +-- .../resolvers/contract/indexContracts.test.ts | 2 +- .../src/resolvers/rate/fetchRate.test.ts | 112 +++++- .../src/resolvers/rate/rateResolver.ts | 187 ++++++---- .../src/resolvers/user/userResolver.ts | 7 +- .../queries/fetchRateWithQuestions.graphql | 319 ++++++++++++++++++ services/app-graphql/src/schema.graphql | 30 ++ 13 files changed, 663 insertions(+), 175 deletions(-) create mode 100644 services/app-graphql/src/queries/fetchRateWithQuestions.graphql diff --git a/services/app-api/src/domain-models/QuestionsType.ts b/services/app-api/src/domain-models/QuestionsType.ts index 898d36614c..ed9d3a59e8 100644 --- a/services/app-api/src/domain-models/QuestionsType.ts +++ b/services/app-api/src/domain-models/QuestionsType.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { cmsUserSchema } from './UserType' +import { cmsUsersUnionSchema } from './UserType' import { questionResponseType } from './QuestionResponseType' import { divisionType } from './DivisionType' @@ -12,7 +12,7 @@ const document = z.object({ const commonQuestionSchema = z.object({ id: z.string().uuid(), createdAt: z.date(), - addedBy: cmsUserSchema, + addedBy: cmsUsersUnionSchema, division: divisionType, // DMCO, DMCP, OACT documents: z.array(document), responses: z.array(questionResponseType), @@ -30,17 +30,32 @@ const questionEdge = z.object({ node: question, }) +const rateQuestionEdge = z.object({ + node: rateQuestion, +}) + const questionList = z.object({ totalCount: z.number(), edges: z.array(questionEdge), }) +const rateQuestionList = z.object({ + totalCount: z.number(), + edges: z.array(rateQuestionEdge), +}) + const indexQuestionsPayload = z.object({ DMCOQuestions: questionList, DMCPQuestions: questionList, OACTQuestions: questionList, }) +const indexRateQuestionsPayload = z.object({ + DMCOQuestions: rateQuestionList, + DMCPQuestions: rateQuestionList, + OACTQuestions: rateQuestionList, +}) + const createQuestionPayload = z.object({ question: question, }) @@ -71,6 +86,8 @@ type QuestionList = z.infer type Document = z.infer +type IndexRateQuestionsPayload = z.infer + export type { IndexQuestionsPayload, CreateQuestionPayload, @@ -80,6 +97,7 @@ export type { QuestionList, RateQuestionType, CreateRateQuestionInputType, + IndexRateQuestionsPayload, } export { diff --git a/services/app-api/src/domain-models/UserType.ts b/services/app-api/src/domain-models/UserType.ts index e1a8259038..bd9cf65aba 100644 --- a/services/app-api/src/domain-models/UserType.ts +++ b/services/app-api/src/domain-models/UserType.ts @@ -44,6 +44,8 @@ const cmsApproverUserSchema = baseUserSchema.extend({ divisionAssignment: divisionType.optional(), }) +const cmsUsersUnionSchema = z.union([cmsUserSchema, cmsApproverUserSchema]) + const adminUserSchema = baseUserSchema.extend({ role: z.literal(userRolesSchema.enum.ADMIN_USER), }) @@ -68,7 +70,7 @@ type CMSUserType = z.infer type CMSApproverUserType = z.infer -type CMSUsersUnionType = CMSUserType | CMSApproverUserType +type CMSUsersUnionType = z.infer type BaseUserType = z.infer @@ -87,4 +89,10 @@ export type { BaseUserType, } -export { cmsUserSchema, stateUserSchema, userRolesSchema, baseUserSchema } +export { + cmsUserSchema, + stateUserSchema, + userRolesSchema, + baseUserSchema, + cmsUsersUnionSchema, +} diff --git a/services/app-api/src/domain-models/index.ts b/services/app-api/src/domain-models/index.ts index c9605de5d7..4c323c6ba5 100644 --- a/services/app-api/src/domain-models/index.ts +++ b/services/app-api/src/domain-models/index.ts @@ -71,6 +71,7 @@ export type { QuestionList, RateQuestionType, CreateRateQuestionInputType, + IndexRateQuestionsPayload, } from './QuestionsType' export type { diff --git a/services/app-api/src/postgres/questionResponse/index.ts b/services/app-api/src/postgres/questionResponse/index.ts index 531882156f..7737cdd376 100644 --- a/services/app-api/src/postgres/questionResponse/index.ts +++ b/services/app-api/src/postgres/questionResponse/index.ts @@ -4,6 +4,7 @@ export { convertToIndexQuestionsPayload, questionPrismaToDomainType, rateQuestionPrismaToDomainType, + convertToIndexRateQuestionsPayload, } from './questionHelpers' export { insertQuestionResponse } from './insertQuestionResponse' export { insertRateQuestion } from './insertRateQuestion' diff --git a/services/app-api/src/postgres/questionResponse/questionHelpers.ts b/services/app-api/src/postgres/questionResponse/questionHelpers.ts index 2e2a719e59..dca5009498 100644 --- a/services/app-api/src/postgres/questionResponse/questionHelpers.ts +++ b/services/app-api/src/postgres/questionResponse/questionHelpers.ts @@ -1,6 +1,7 @@ import type { - CMSUserType, + CMSUsersUnionType, IndexQuestionsPayload, + IndexRateQuestionsPayload, Question, QuestionResponseType, RateQuestionType, @@ -22,7 +23,11 @@ const questionInclude = { createdAt: 'desc', }, }, - addedBy: true, + addedBy: { + include: { + stateAssignments: true, + }, + }, } satisfies Prisma.QuestionInclude type PrismaQuestionType = Prisma.QuestionGetPayload<{ @@ -35,66 +40,60 @@ type PrismaRateQuestionType = Prisma.RateQuestionGetPayload<{ // Both types are similar only difference is one related to a contract and the other a rate. const commonQuestionPrismaToDomainType = < - T extends PrismaQuestionType | PrismaRateQuestionType, - U extends Question | RateQuestionType, + P extends PrismaQuestionType | PrismaRateQuestionType, + R extends Question | RateQuestionType, >( - prismaQuestion: T -): U => + prismaQuestion: P +): R => ({ ...prismaQuestion, - addedBy: { - ...prismaQuestion.addedBy, - stateAssignments: [], - } as CMSUserType, + addedBy: prismaQuestion.addedBy as CMSUsersUnionType, responses: prismaQuestion.responses as QuestionResponseType[], - }) as unknown as U + }) as unknown as R -const questionPrismaToDomainType = commonQuestionPrismaToDomainType< - PrismaQuestionType, - Question -> -const rateQuestionPrismaToDomainType = commonQuestionPrismaToDomainType< - PrismaRateQuestionType, - RateQuestionType -> +const questionPrismaToDomainType = ( + prismaQuestion: PrismaQuestionType +): Question => commonQuestionPrismaToDomainType(prismaQuestion) +const rateQuestionPrismaToDomainType = ( + prismaQuestion: PrismaRateQuestionType +): RateQuestionType => commonQuestionPrismaToDomainType(prismaQuestion) -const convertToIndexQuestionsPayload = ( - questions: Question[] -): IndexQuestionsPayload => { - const questionsPayload: IndexQuestionsPayload = { - DMCOQuestions: { - totalCount: 0, - edges: [], - }, - DMCPQuestions: { - totalCount: 0, - edges: [], - }, - OACTQuestions: { - totalCount: 0, - edges: [], - }, - } - - questions.forEach((question) => { - if (question.division === 'DMCP') { - questionsPayload.DMCPQuestions.edges.push({ node: question }) - questionsPayload.DMCPQuestions.totalCount++ - } else if (question.division === 'OACT') { - questionsPayload.OACTQuestions.edges.push({ node: question }) - questionsPayload.OACTQuestions.totalCount++ - } else if (question.division === 'DMCO') { - questionsPayload.DMCOQuestions.edges.push({ node: question }) - questionsPayload.DMCOQuestions.totalCount++ - } +const convertToCommonIndexQuestionsPayload = < + P extends Question | RateQuestionType, + R extends IndexQuestionsPayload | IndexRateQuestionsPayload, +>( + questions: P[] +): R => { + const getDivisionQuestionsEdge = ( + division: 'DMCP' | 'DMCO' | 'OACT', + questions: P[] + ) => ({ + totalCount: questions.filter((q) => q.division === division).length, + edges: questions + .filter((q) => q.division === division) + .map((question) => ({ node: question })), }) - return questionsPayload + return { + DMCOQuestions: getDivisionQuestionsEdge('DMCO', questions), + DMCPQuestions: getDivisionQuestionsEdge('DMCP', questions), + OACTQuestions: getDivisionQuestionsEdge('OACT', questions), + } as unknown as R } +const convertToIndexQuestionsPayload = ( + contractQuestions: Question[] +): IndexQuestionsPayload => + convertToCommonIndexQuestionsPayload(contractQuestions) +const convertToIndexRateQuestionsPayload = ( + rateQuestions: RateQuestionType[] +): IndexRateQuestionsPayload => + convertToCommonIndexQuestionsPayload(rateQuestions) + export { questionInclude, questionPrismaToDomainType, convertToIndexQuestionsPayload, + convertToIndexRateQuestionsPayload, rateQuestionPrismaToDomainType, } diff --git a/services/app-api/src/resolvers/configureResolvers.ts b/services/app-api/src/resolvers/configureResolvers.ts index e933628cf2..da1832916b 100644 --- a/services/app-api/src/resolvers/configureResolvers.ts +++ b/services/app-api/src/resolvers/configureResolvers.ts @@ -185,7 +185,7 @@ export function configureResolvers( CMSUser: cmsUserResolver, CMSApproverUser: cmsApproverUserResolver, HealthPlanPackage: healthPlanPackageResolver(store), - Rate: rateResolver, + Rate: rateResolver(store), RateRevision: rateRevisionResolver(store), Contract: contractResolver(store), UnlockedContract: unlockedContractResolver(), diff --git a/services/app-api/src/resolvers/contract/contractResolver.ts b/services/app-api/src/resolvers/contract/contractResolver.ts index 678c9a88c7..0b70bc6d04 100644 --- a/services/app-api/src/resolvers/contract/contractResolver.ts +++ b/services/app-api/src/resolvers/contract/contractResolver.ts @@ -11,7 +11,7 @@ import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, } from '../attributeHelper' -import type { IndexQuestionsPayload } from '../../domain-models/QuestionsType' +import { convertToIndexQuestionsPayload } from '../../postgres/questionResponse' export function contractResolver(store: Store): Resolvers['Contract'] { return { @@ -145,49 +145,7 @@ export function contractResolver(store: Store): Resolvers['Contract'] { }) } - const dmcoQuestions = questionsForContract - .filter((question) => question.division === 'DMCO') - .map((question) => { - return { - node: { - ...question, - }, - } - }) - const dmcpQuestions = questionsForContract - .filter((question) => question.division === 'DMCP') - .map((question) => { - return { - node: { - ...question, - }, - } - }) - const oactQuestions = questionsForContract - .filter((question) => question.division === 'OACT') - .map((question) => { - return { - node: { - ...question, - }, - } - }) - - const questionPayload: IndexQuestionsPayload = { - DMCOQuestions: { - totalCount: dmcoQuestions.length, - edges: dmcoQuestions, - }, - DMCPQuestions: { - totalCount: dmcpQuestions.length, - edges: dmcpQuestions, - }, - OACTQuestions: { - totalCount: oactQuestions.length, - edges: oactQuestions, - }, - } - return questionPayload + return convertToIndexQuestionsPayload(questionsForContract) }, } } diff --git a/services/app-api/src/resolvers/contract/indexContracts.test.ts b/services/app-api/src/resolvers/contract/indexContracts.test.ts index 79efe47fff..1227487d79 100644 --- a/services/app-api/src/resolvers/contract/indexContracts.test.ts +++ b/services/app-api/src/resolvers/contract/indexContracts.test.ts @@ -1,4 +1,4 @@ -import INDEX_CONTRACTS from '../../../../app-graphql/src/queries/indexContracts.graphql' +import INDEX_CONTRACTS from '../../../../app-graphql/src/queries/indexContractsForDashboard.graphql' import { constructTestPostgresServer, createTestHealthPlanPackage, diff --git a/services/app-api/src/resolvers/rate/fetchRate.test.ts b/services/app-api/src/resolvers/rate/fetchRate.test.ts index 63439251d0..562aeab738 100644 --- a/services/app-api/src/resolvers/rate/fetchRate.test.ts +++ b/services/app-api/src/resolvers/rate/fetchRate.test.ts @@ -1,12 +1,18 @@ import FETCH_RATE from '../../../../app-graphql/src/queries/fetchRate.graphql' +import FETCH_RATE_WITH_QUESTIONS from '../../../../app-graphql/src/queries/fetchRateWithQuestions.graphql' import { testLDService } from '../../testHelpers/launchDarklyHelpers' import { constructTestPostgresServer, + createTestRateQuestion, defaultFloridaRateProgram, unlockTestHealthPlanPackage, updateTestHealthPlanFormData, } from '../../testHelpers/gqlHelpers' -import { testCMSUser } from '../../testHelpers/userHelpers' +import { + createDBUsersWithFullData, + testCMSApproverUser, + testCMSUser, +} from '../../testHelpers/userHelpers' import { submitTestRate, updateTestRate } from '../../testHelpers' import { v4 as uuidv4 } from 'uuid' import { @@ -18,6 +24,7 @@ import { } from '../../testHelpers/gqlRateHelpers' import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' import { + createAndSubmitTestContractWithRate, createAndUpdateTestContractWithoutRates, fetchTestContract, submitTestContract, @@ -455,4 +462,107 @@ describe('fetchRate', () => { .format('YYYY-MM-DD') ).toBe('2024-02-02') }) + + it('returns the questions on for a rate', async () => { + // Create four CMS users, seed and assign divisions + const dmcoCmsUser = testCMSUser() + const dmco2CmsUser = testCMSUser({ + role: 'CMS_USER', + email: 'zuko2@example.com', + familyName: 'Zuko2', + givenName: 'Prince', + divisionAssignment: 'DMCO' as const, + }) + const oactApproverUser = testCMSApproverUser({ + divisionAssignment: 'OACT', + }) + const dmcpCmsUser = testCMSUser({ + divisionAssignment: 'DMCP', + }) + await createDBUsersWithFullData([ + dmcoCmsUser, + oactApproverUser, + dmcpCmsUser, + dmco2CmsUser, + ]) + + // Create servers + const server = await constructTestPostgresServer() + const dmcoServer = await constructTestPostgresServer({ + context: { + user: dmcoCmsUser, + }, + }) + const dmco2Server = await constructTestPostgresServer({ + context: { + user: dmco2CmsUser, + }, + }) + const dmcpServer = await constructTestPostgresServer({ + context: { + user: dmcpCmsUser, + }, + }) + const oactServer = await constructTestPostgresServer({ + context: { + user: oactApproverUser, + }, + }) + + // Set up contract and rate submission and submit 1 question for each division + const submittedRate = await createAndSubmitTestContractWithRate(server) + const rateID = + submittedRate.packageSubmissions[0].rateRevisions[0].rateID + + await createTestRateQuestion(dmcoServer, rateID) + await createTestRateQuestion(dmcpServer, rateID) + await createTestRateQuestion(oactServer, rateID) + + const result = await server.executeOperation({ + query: FETCH_RATE_WITH_QUESTIONS, + variables: { + input: { + rateID, + }, + }, + }) + const rateQuestions = result.data?.fetchRate.rate.questions + + // Expect each question in the correct division by the correct user + expect(rateQuestions.DMCOQuestions.edges).toHaveLength(1) + expect(rateQuestions.DMCOQuestions.edges[0].node.addedBy).toEqual( + expect.objectContaining(dmcoCmsUser) + ) + expect(rateQuestions.DMCPQuestions.edges).toHaveLength(1) + expect(rateQuestions.DMCPQuestions.edges[0].node.addedBy).toEqual( + expect.objectContaining(dmcpCmsUser) + ) + expect(rateQuestions.OACTQuestions.edges).toHaveLength(1) + expect(rateQuestions.OACTQuestions.edges[0].node.addedBy).toEqual( + expect.objectContaining(oactApproverUser) + ) + + // Test newly created dmco question and its order + await createTestRateQuestion(dmco2Server, rateID) + const result2 = await server.executeOperation({ + query: FETCH_RATE_WITH_QUESTIONS, + variables: { + input: { + rateID, + }, + }, + }) + const rateQuestions2 = result2.data?.fetchRate.rate.questions + + // Expect 2 DMCO questions and the latest created question at index 0 by dmco2CmsUser + expect(rateQuestions2.DMCOQuestions.edges).toHaveLength(2) + expect(rateQuestions2.DMCOQuestions.edges[0].node.addedBy).toEqual( + expect.objectContaining(dmco2CmsUser) + ) + // Expect earlier DMCO question to be at index 1 + expect(rateQuestions2.DMCOQuestions.edges).toHaveLength(2) + expect(rateQuestions2.DMCOQuestions.edges[1].node.addedBy).toEqual( + expect.objectContaining(dmcoCmsUser) + ) + }) }) diff --git a/services/app-api/src/resolvers/rate/rateResolver.ts b/services/app-api/src/resolvers/rate/rateResolver.ts index aa315a680d..9e48141fb3 100644 --- a/services/app-api/src/resolvers/rate/rateResolver.ts +++ b/services/app-api/src/resolvers/rate/rateResolver.ts @@ -6,6 +6,13 @@ import type { RatePackageSubmissionWithCauseType, RateType, } from '../../domain-models' +import { + setErrorAttributesOnActiveSpan, + setResolverDetailsOnActiveSpan, +} from '../attributeHelper' +import type { Store } from '../../postgres' +import { NotFoundError } from '../../postgres' +import { convertToIndexRateQuestionsPayload } from '../../postgres/questionResponse' // Return the date of the first submission for a rate // This method relies on revisions always being presented in most-recent-first order @@ -14,86 +21,126 @@ function initialSubmitDate(rate: RateType): Date | undefined { return firstSubmittedRev?.submitInfo?.updatedAt } -export const rateResolver: Resolvers['Rate'] = { - initiallySubmittedAt(parent) { - return initialSubmitDate(parent) || null - }, - state(parent) { - const packageState = parent.stateCode - const state = statePrograms.states.find( - (st) => st.code === packageState - ) +export function rateResolver(store: Store): Resolvers['Rate'] { + return { + initiallySubmittedAt(parent) { + return initialSubmitDate(parent) || null + }, + state(parent) { + const packageState = parent.stateCode + const state = statePrograms.states.find( + (st) => st.code === packageState + ) - if (state === undefined) { - const errMessage = 'State not found in database: ' + packageState - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - return state - }, - packageSubmissions(parent) { - const gqlSubs: RatePackageSubmissionWithCauseType[] = [] - for (let i = 0; i < parent.packageSubmissions.length; i++) { - const thisSub = parent.packageSubmissions[i] - let prevSub = undefined - if (i < parent.packageSubmissions.length - 1) { - prevSub = parent.packageSubmissions[i + 1] + if (state === undefined) { + const errMessage = + 'State not found in database: ' + packageState + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) } + return state + }, + packageSubmissions(parent) { + const gqlSubs: RatePackageSubmissionWithCauseType[] = [] + for (let i = 0; i < parent.packageSubmissions.length; i++) { + const thisSub = parent.packageSubmissions[i] + let prevSub = undefined + if (i < parent.packageSubmissions.length - 1) { + prevSub = parent.packageSubmissions[i + 1] + } - // determine the cause for this submission - let cause: SubmissionReason = 'RATE_SUBMISSION' + // determine the cause for this submission + let cause: SubmissionReason = 'RATE_SUBMISSION' - if ( - !thisSub.submittedRevisions.find( - (r) => r.id === thisSub.rateRevision.id - ) - ) { - // not a rate submission, this rate wasn't in the submitted bits - const connectedContractRevisionIDs = - thisSub.contractRevisions.map((r) => r.id) - const submittedContract = thisSub.submittedRevisions.find((r) => - connectedContractRevisionIDs.includes(r.id) - ) + if ( + !thisSub.submittedRevisions.find( + (r) => r.id === thisSub.rateRevision.id + ) + ) { + // not a rate submission, this rate wasn't in the submitted bits + const connectedContractRevisionIDs = + thisSub.contractRevisions.map((r) => r.id) + const submittedContract = thisSub.submittedRevisions.find( + (r) => connectedContractRevisionIDs.includes(r.id) + ) - if (!submittedContract) { - cause = 'RATE_UNLINK' - } else { - const thisSubmittedContract = - submittedContract as ContractRevisionType - if (!prevSub) { - throw new Error( - 'Programming Error: a non-rate submission must have a previous rate submission' - ) - } - const previousContractRevisionIDs = - prevSub.contractRevisions.map((r) => r.contract.id) - if ( - previousContractRevisionIDs.includes( - thisSubmittedContract.contract.id - ) - ) { - cause = 'CONTRACT_SUBMISSION' + if (!submittedContract) { + cause = 'RATE_UNLINK' } else { - cause = 'RATE_LINK' + const thisSubmittedContract = + submittedContract as ContractRevisionType + if (!prevSub) { + throw new Error( + 'Programming Error: a non-rate submission must have a previous rate submission' + ) + } + const previousContractRevisionIDs = + prevSub.contractRevisions.map((r) => r.contract.id) + if ( + previousContractRevisionIDs.includes( + thisSubmittedContract.contract.id + ) + ) { + cause = 'CONTRACT_SUBMISSION' + } else { + cause = 'RATE_LINK' + } } } - } - const gqlSub: RatePackageSubmissionWithCauseType = { - cause, - submitInfo: thisSub.submitInfo, - submittedRevisions: thisSub.submittedRevisions, - rateRevision: thisSub.rateRevision, - contractRevisions: thisSub.contractRevisions, + const gqlSub: RatePackageSubmissionWithCauseType = { + cause, + submitInfo: thisSub.submitInfo, + submittedRevisions: thisSub.submittedRevisions, + rateRevision: thisSub.rateRevision, + contractRevisions: thisSub.contractRevisions, + } + + gqlSubs.push(gqlSub) } - gqlSubs.push(gqlSub) - } + return gqlSubs + }, + questions: async (parent, _args, context) => { + const { user, ctx, tracer } = context + // add a span to OTEL + const span = tracer?.startSpan( + 'fetchRateWithQuestionsResolver', + {}, + ctx + ) + setResolverDetailsOnActiveSpan('fetchRateWithQuestions', user, span) + + const questionsForRate = await store.findAllQuestionsByRate( + parent.id + ) + + if (questionsForRate instanceof Error) { + const errMessage = `Issue finding questions for rate. Message: ${questionsForRate.message}` + setErrorAttributesOnActiveSpan(errMessage, span) + + if (questionsForRate instanceof NotFoundError) { + throw new GraphQLError(errMessage, { + extensions: { + code: 'NOT_FOUND', + cause: 'DB_ERROR', + }, + }) + } + + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - return gqlSubs - }, + return convertToIndexRateQuestionsPayload(questionsForRate) + }, + } } diff --git a/services/app-api/src/resolvers/user/userResolver.ts b/services/app-api/src/resolvers/user/userResolver.ts index f3eeb7d804..20fa412425 100644 --- a/services/app-api/src/resolvers/user/userResolver.ts +++ b/services/app-api/src/resolvers/user/userResolver.ts @@ -1,11 +1,8 @@ import type { Resolvers } from '../../gen/gqlServer' import statePrograms from '../../../../app-web/src/common-code/data/statePrograms.json' -import type { - CMSUserType, - CMSApproverUserType, -} from '../../domain-models/UserType' +import type { CMSUsersUnionType } from '../../domain-models/UserType' -function getStateAssignments(user: CMSUserType | CMSApproverUserType) { +function getStateAssignments(user: CMSUsersUnionType) { const userStates = user.stateAssignments const statesWithPrograms = userStates.map((userState) => { const state = statePrograms.states.find( diff --git a/services/app-graphql/src/queries/fetchRateWithQuestions.graphql b/services/app-graphql/src/queries/fetchRateWithQuestions.graphql new file mode 100644 index 0000000000..6135f18fe1 --- /dev/null +++ b/services/app-graphql/src/queries/fetchRateWithQuestions.graphql @@ -0,0 +1,319 @@ +query fetchRateWithQuestions($input: FetchRateInput!) { + fetchRate(input: $input) { + rate { + ...rateFieldsForFetchRate + + draftRevision { + ...rateRevisionFragmentForFetchRate + } + + revisions { + ...rateRevisionFragmentForFetchRate + } + questions { + DMCOQuestions { + edges { + ...rateQuestionEdgeFragment + } + } + DMCPQuestions { + edges { + ...rateQuestionEdgeFragment + } + } + OACTQuestions { + edges { + ...rateQuestionEdgeFragment + } + } + } + } + } +} + +fragment rateRevisionFragmentForFetchRate on RateRevision { + id + rateID + createdAt + updatedAt + unlockInfo { + ...updateInformationFields + } + submitInfo { + ...updateInformationFields + } + formData { + rateType + rateCapitationType + rateDocuments { + name + s3URL + sha256 + dateAdded + downloadURL + } + supportingDocuments { + name + s3URL + sha256 + dateAdded + downloadURL + } + rateDateStart + rateDateEnd + rateDateCertified + amendmentEffectiveDateStart + amendmentEffectiveDateEnd + rateProgramIDs + deprecatedRateProgramIDs + rateCertificationName + certifyingActuaryContacts { + id + name + titleRole + email + actuarialFirm + actuarialFirmOther + } + addtlActuaryContacts { + id + name + titleRole + email + actuarialFirm + actuarialFirmOther + } + actuaryCommunicationPreference + packagesWithSharedRateCerts { + packageName + packageId + packageStatus + } + } +} + +fragment rateFieldsForFetchRate on Rate { + id + createdAt + updatedAt + stateCode + stateNumber + parentContractID + state { + code + name + programs { + id + name + fullName + isRateProgram + } + } + status + parentContractID + initiallySubmittedAt + withdrawInfo { + ...updateInformationFields + } + packageSubmissions { + ...packageSubmissionsFragmentForFetchRate + } +} + +fragment packageSubmissionsFragmentForFetchRate on RatePackageSubmission { + cause + submitInfo { + ...updateInformationFields + } + + submittedRevisions { + ...submittableRevisionsFieldsForFetchRate + } + + rateRevision { + ...rateRevisionFragmentForFetchRate + } + + contractRevisions { + ...contractRevisionFragmentForFetchRate + } +} + +fragment contractFormDataFragmentForFetchRate on ContractFormData { + 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 + modifiedMedicaidBeneficiaries + modifiedMedicalLossRatioStandards + modifiedOtherFinancialPaymentIncentive + modifiedEnrollmentProcess + modifiedGrevienceAndAppeal + modifiedNetworkAdequacyStandards + modifiedLengthOfContract + modifiedNonRiskPaymentArrangements + statutoryRegulatoryAttestation + statutoryRegulatoryAttestationDescription +} + +fragment contractRevisionFragmentForFetchRate on ContractRevision { + id + createdAt + updatedAt + contractID + contractName + + submitInfo { + ...updateInformationFields + } + + unlockInfo { + ...updateInformationFields + } + + formData { + ...contractFormDataFragmentForFetchRate + } +} + +fragment submittableRevisionsFieldsForFetchRate on SubmittableRevision { + ... on ContractRevision { + ...contractRevisionFragmentForFetchRate + } + ... on RateRevision { + ...rateRevisionFragmentForFetchRate + } +} + + +fragment updateInformationFields on UpdateInformation { + updatedAt + updatedBy { + email + role + familyName + givenName + } + updatedReason +} + +fragment rateQuestionEdgeFragment on RateQuestionEdge { + node { + id + rateID + createdAt + addedBy { + ... on CMSUser { + id + email + role + familyName + givenName + stateAssignments { + code + name + programs { + id + name + fullName + isRateProgram + } + } + divisionAssignment + } + ... on CMSApproverUser { + id + email + role + familyName + givenName + stateAssignments { + code + name + programs { + id + name + fullName + isRateProgram + } + } + divisionAssignment + } + } + division + documents { + s3URL + name + downloadURL + } + responses { + id + questionID + createdAt + addedBy { + id + role + email + givenName + familyName + state { + code + name + programs { + id + name + fullName + isRateProgram + } + } + } + documents { + name + s3URL + downloadURL + } + } + } +} diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index aab7c6f1fb..d77d11e09c 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -648,15 +648,33 @@ type IndexQuestionsPayload { OACTQuestions: QuestionList! } +type IndexRateQuestionsPayload { + "Questions for a given rate that were asked by DMCO within CMS" + DMCOQuestions: RateQuestionList! + "Questions for a given rate that were asked by DMCP within CMS" + DMCPQuestions: RateQuestionList! + "Questions for a given rate that were asked by OACT within CMS" + OACTQuestions: RateQuestionList! +} + type QuestionList { totalCount: Int edges: [QuestionEdge!]! } +type RateQuestionList { + totalCount: Int + edges: [RateQuestionEdge!]! +} + type QuestionEdge { node: Question! } +type RateQuestionEdge { + node: RateQuestion! +} + input DocumentInput { "The name of the document" name: String! @@ -1642,6 +1660,12 @@ type Rate { was associated with. """ packageSubmissions: [RatePackageSubmission!] + """ + questions field is an array of questions asked about the rate by CMS. Each questions also contains responses + to the question submitted by the State. DRAFT rates will not have questions, only rates that have been submitted + , unlocked, or resubmitted. The array is in descending order by createdAt. + """ + questions: IndexRateQuestionsPayload } """ @@ -1760,6 +1784,12 @@ type Contract { submission is in the first position in the array. """ packageSubmissions: [ContractPackageSubmission!]! + + """ + questions field is an array of questions asked about the contract by CMS. Each questions also contains responses + to the question submitted by the State. DRAFT contracts will not have questions, only contracts that have been submitted + , unlocked, or resubmitted. The array is in descending order by createdAt. + """ questions: IndexQuestionsPayload }