From e72775d6e0df6c4405a26be0bfc85b1cfddee18f Mon Sep 17 00:00:00 2001 From: Ben Harvey Date: Mon, 17 Jul 2023 13:46:00 -0400 Subject: [PATCH 1/3] Update sechub/jira sync repo ref to new name (#1811) --- .github/workflows/sechub-jira-sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sechub-jira-sync.yml b/.github/workflows/sechub-jira-sync.yml index 5c11056382..a242609a9a 100644 --- a/.github/workflows/sechub-jira-sync.yml +++ b/.github/workflows/sechub-jira-sync.yml @@ -25,7 +25,7 @@ jobs: stage-name: prod - name: Sync Security Hub and Jira - uses: Enterprise-CMCS/security-hub-visibility@v1.0.1 + uses: Enterprise-CMCS/mac-fc-security-hub-visibility@v1.0.1 with: jira-token: ${{ secrets.JIRA_TOKEN }} jira-username: ${{ secrets.JIRA_USERNAME }} From 7d9ef9a024900833876ac01ce3c3d0c3bf198501 Mon Sep 17 00:00:00 2001 From: Mazdak Atighi Date: Tue, 18 Jul 2023 12:44:18 -0500 Subject: [PATCH 2/3] MR-3212: Create a lambda to run migrations (#1810) --- .github/workflows/deploy-app-to-env.yml | 10 ++- .github/workflows/deploy.yml | 2 +- .../app-api/scripts/invoke-migrate-lambda.sh | 5 +- services/app-api/serverless.yml | 11 +++ services/app-api/src/handlers/proto_to_db.ts | 82 +++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 services/app-api/src/handlers/proto_to_db.ts diff --git a/.github/workflows/deploy-app-to-env.yml b/.github/workflows/deploy-app-to-env.yml index 3eab0a04bf..b414e15570 100644 --- a/.github/workflows/deploy-app-to-env.yml +++ b/.github/workflows/deploy-app-to-env.yml @@ -135,4 +135,12 @@ jobs: env: STAGE_NAME: ${{ inputs.stage_name }} run: | - ./scripts/invoke-migrate-lambda.sh app-api-$STAGE_NAME-migrate + ./scripts/invoke-migrate-lambda.sh app-api-$STAGE_NAME-migrate \$LATEST "Migration of the database failed." + + - name: invoke proto_to_db lambda + id: invoke-proto_to_db + working-directory: services/app-api + env: + STAGE_NAME: ${{ inputs.stage_name }} + run: | + ./scripts/invoke-migrate-lambda.sh app-api-$STAGE_NAME-proto_to_db \$LATEST "Migration of the protos to the database failed." diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c390bf3e0..583395812b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -277,7 +277,7 @@ jobs: needs.api-unit-tests.result == 'success' && needs.build-prisma-client-lambda-layer.result == 'success' && needs.begin-deployment.result == 'success' - uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-app-to-env.yml@main + uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-app-to-env.yml@ma_3212_migration_lambda with: environment: dev stage_name: ${{ needs.begin-deployment.outputs.stage-name }} diff --git a/services/app-api/scripts/invoke-migrate-lambda.sh b/services/app-api/scripts/invoke-migrate-lambda.sh index 65aa5791ce..176baecd75 100755 --- a/services/app-api/scripts/invoke-migrate-lambda.sh +++ b/services/app-api/scripts/invoke-migrate-lambda.sh @@ -3,6 +3,7 @@ set -u function_name="$1" lambda_version="${2:-\$LATEST}" +error_message="${3:-}" cli_read_timeout=240 @@ -10,9 +11,9 @@ if (set -x ; aws lambda invoke --qualifier "$lambda_version" --cli-read-timeout exitCode="$(jq '.statusCode' < lambda_response.json)" if [[ "$exitCode" != 200 ]] ; then cat lambda_response.json - echo "Migration of the database failed." 1>&2 + echo "$error_message" 1>&2 exit 1 fi else exit -fi \ No newline at end of file +fi diff --git a/services/app-api/serverless.yml b/services/app-api/serverless.yml index a2f7b69eae..da368f62a2 100644 --- a/services/app-api/serverless.yml +++ b/services/app-api/serverless.yml @@ -163,6 +163,17 @@ functions: - ${self:custom.sgId} subnetIds: ${self:custom.privateSubnets} + proto_to_db: + handler: src/handlers/proto_to_db.main + layers: + - !Ref PrismaClientEngineLambdaLayer + - arn:aws:lambda:us-east-1:901920570463:layer:aws-otel-nodejs-amd64-ver-1-9-1:1 + timeout: 300 + vpc: + securityGroupIds: + - ${self:custom.sgId} + subnetIds: ${self:custom.privateSubnets} + migrate_rate_documents: handler: src/handlers/migrate_rate_documents.main layers: diff --git a/services/app-api/src/handlers/proto_to_db.ts b/services/app-api/src/handlers/proto_to_db.ts new file mode 100644 index 0000000000..82ef325edc --- /dev/null +++ b/services/app-api/src/handlers/proto_to_db.ts @@ -0,0 +1,82 @@ +import { Handler } from 'aws-lambda' +import { initTracer, initMeter } from '../../../uploads/src/lib/otel' +import { configurePostgres } from './configuration' +import { NewPostgresStore } from '../postgres/postgresStore' +import { Store } from '../postgres' +import { HealthPlanRevisionTable } from '@prisma/client' +import { isStoreError, StoreError } from '../postgres/storeError' + +export const getDatabaseConnection = async (): Promise => { + const dbURL = process.env.DATABASE_URL + const secretsManagerSecret = process.env.SECRETS_MANAGER_SECRET + + if (!dbURL) { + console.error('DATABASE_URL not set') + throw new Error('Init Error: DATABASE_URL is required to run app-api') + } + if (!secretsManagerSecret) { + console.error('SECRETS_MANAGER_SECRET not set') + } + + const pgResult = await configurePostgres(dbURL, secretsManagerSecret) + if (pgResult instanceof Error) { + console.error( + "Init Error: Postgres couldn't be configured in data exporter" + ) + throw pgResult + } else { + console.info('Postgres configured in data exporter') + } + const store = NewPostgresStore(pgResult) + + return store +} + +export const getRevisions = async ( + store: Store +): Promise => { + const result: HealthPlanRevisionTable[] | StoreError = + await store.findAllRevisions() + if (isStoreError(result)) { + console.error( + `Error getting revisions from db ${JSON.stringify(result)}` + ) + throw new Error('Error getting records; cannot generate report') + } + + return result +} + +export const main: Handler = async (event, context) => { + // Check on the values for our required config + const stageName = process.env.stage ?? 'stageNotSet' + const serviceName = `proto_to_db_lambda-${stageName}` + const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL + if (otelCollectorURL) { + initTracer(serviceName, otelCollectorURL) + } else { + console.error( + 'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set' + ) + } + + initMeter(serviceName) + const store = await getDatabaseConnection() + + const revisions = await getRevisions(store) + // Get the pkgID from the first revision in the list + const pkgID = revisions[0].pkgID + if (!pkgID) { + console.error('Package ID is missing in the revisions') + throw new Error('Package ID is required') + } + console.info(`Package ID: ${pkgID}`) + + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Lambda function executed successfully', + packageId: pkgID, + }), + } +} From 3dcec7a2bad3ab18e0d4a067f7e60c0fffd98d13 Mon Sep 17 00:00:00 2001 From: Jason Lin <98117700+JasonLin0991@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:06:39 -0400 Subject: [PATCH 3/3] MR-3429: createHealthPlanPackage resolver returns data inserted insertContract (#1813) * Add zod schema and generate contract types from it. Remove contractType.ts * Add prisma types and update rate type to use zod schema types. * Split converting prisma to domain and parsing data functions into different files. * Use new domain data parsing functions to validate data. * Update tests and other handlers to use zod generated types. * Change state because tests run in parallel which affects count. * Add rates-db-refactor flag * Add insertContract to postgresStore.ts and storeHelpers.ts * Repeatable tests with feature flag state as parameters. --- .../src/domain-models/healthPlanPackage.ts | 146 +++++++++++++++ services/app-api/src/domain-models/index.ts | 1 + .../contractAndRates/insertContract.ts | 4 +- .../insertHealthPlanPackage.test.ts | 1 + .../app-api/src/postgres/postgresStore.ts | 13 ++ .../src/resolvers/configureResolvers.ts | 5 +- .../createHealthPlanPackage.test.ts | 174 +++++++++++------- .../createHealthPlanPackage.ts | 81 ++++++-- .../app-api/src/testHelpers/storeHelpers.ts | 5 + .../src/common-code/featureFlags/flags.ts | 7 + 10 files changed, 346 insertions(+), 91 deletions(-) diff --git a/services/app-api/src/domain-models/healthPlanPackage.ts b/services/app-api/src/domain-models/healthPlanPackage.ts index f4a3b817b9..053fec6792 100644 --- a/services/app-api/src/domain-models/healthPlanPackage.ts +++ b/services/app-api/src/domain-models/healthPlanPackage.ts @@ -4,6 +4,15 @@ import { HealthPlanPackageType, } from './HealthPlanPackageType' import { pruneDuplicateEmails } from '../emailer/formatters' +import { ContractType } from './contractAndRates/contractAndRatesZodSchema' +import { + SubmissionDocument, + UnlockedHealthPlanFormDataType, +} from '../../../app-web/src/common-code/healthPlanFormDataType' +import { + toProtoBuffer, + toDomain, +} from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' // submissionStatus computes the current status of the submission based on // the submit/unlock info on its revisions. @@ -60,9 +69,146 @@ function packageSubmitters(pkg: HealthPlanPackageType): string[] { return pruneDuplicateEmails(submitters) } +function convertContractToUnlockedHealthPlanPackage( + contract: ContractType +): HealthPlanPackageType | Error { + console.info('Attempting to convert contract to health plan package') + + const healthPlanRevisions = + convertContractRevisionToHealthPlanRevision(contract) + + if (healthPlanRevisions instanceof Error) { + return healthPlanRevisions + } + + return { + id: contract.id, + stateCode: contract.stateCode, + revisions: healthPlanRevisions, + } +} + +function convertContractRevisionToHealthPlanRevision( + contract: ContractType +): HealthPlanRevisionType[] | Error { + if (contract.status !== 'DRAFT') { + return new Error( + `Contract with ID: ${contract.id} status is not "DRAFT". Cannot convert to unlocked health plan package` + ) + } + + let healthPlanRevisions: HealthPlanRevisionType[] | Error = [] + for (const contractRev of contract.revisions) { + const unlockedHealthPlanFormData: UnlockedHealthPlanFormDataType = { + id: contractRev.id, + createdAt: contractRev.createdAt, + updatedAt: contractRev.updatedAt, + status: contract.status, + stateCode: contract.stateCode, + stateNumber: contract.stateNumber, + programIDs: contractRev.formData.programIDs, + populationCovered: contractRev.formData.populationCovered, + submissionType: contractRev.formData.submissionType, + riskBasedContract: contractRev.formData.riskBasedContract, + submissionDescription: contractRev.formData.submissionDescription, + stateContacts: contractRev.formData.stateContacts, + addtlActuaryCommunicationPreference: undefined, + addtlActuaryContacts: [], + documents: contractRev.formData.supportingDocuments.map((doc) => ({ + ...doc, + documentCategories: doc.documentCategories.filter( + (category) => category !== undefined + ), + })) as SubmissionDocument[], + contractType: contractRev.formData.contractType, + contractExecutionStatus: + contractRev.formData.contractExecutionStatus, + contractDocuments: contractRev.formData.contractDocuments.map( + (doc) => ({ + ...doc, + documentCategories: doc.documentCategories.filter( + (category) => category !== undefined + ), + }) + ) as SubmissionDocument[], + contractDateStart: contractRev.formData.contractDateStart, + contractDateEnd: contractRev.formData.contractDateEnd, + managedCareEntities: contractRev.formData.managedCareEntities, + federalAuthorities: contractRev.formData.federalAuthorities, + contractAmendmentInfo: { + modifiedProvisions: { + inLieuServicesAndSettings: + contractRev.formData.inLieuServicesAndSettings, + modifiedBenefitsProvided: + contractRev.formData.modifiedBenefitsProvided, + modifiedGeoAreaServed: + contractRev.formData.modifiedGeoAreaServed, + modifiedMedicaidBeneficiaries: + contractRev.formData.modifiedMedicaidBeneficiaries, + modifiedRiskSharingStrategy: + contractRev.formData.modifiedRiskSharingStrategy, + modifiedIncentiveArrangements: + contractRev.formData.modifiedIncentiveArrangements, + modifiedWitholdAgreements: + contractRev.formData.modifiedWitholdAgreements, + modifiedStateDirectedPayments: + contractRev.formData.modifiedStateDirectedPayments, + modifiedPassThroughPayments: + contractRev.formData.modifiedPassThroughPayments, + modifiedPaymentsForMentalDiseaseInstitutions: + contractRev.formData + .modifiedPaymentsForMentalDiseaseInstitutions, + modifiedMedicalLossRatioStandards: + contractRev.formData.modifiedMedicalLossRatioStandards, + modifiedOtherFinancialPaymentIncentive: + contractRev.formData + .modifiedOtherFinancialPaymentIncentive, + modifiedEnrollmentProcess: + contractRev.formData.modifiedEnrollmentProcess, + modifiedGrevienceAndAppeal: + contractRev.formData.modifiedGrevienceAndAppeal, + modifiedNetworkAdequacyStandards: + contractRev.formData.modifiedNetworkAdequacyStandards, + modifiedLengthOfContract: + contractRev.formData.modifiedLengthOfContract, + modifiedNonRiskPaymentArrangements: + contractRev.formData.modifiedNonRiskPaymentArrangements, + }, + }, + rateInfos: [], + } + + const formDataProto = toProtoBuffer(unlockedHealthPlanFormData) + + // check that we can encode then decode with no issues + const domainData = toDomain(formDataProto) + + // If any revision has en error in decoding we break the loop and return an error + if (domainData instanceof Error) { + healthPlanRevisions = new Error( + `Could not convert contract revision with ID: ${contractRev.id} to health plan package revision: ${domainData}` + ) + break + } + + const healthPlanRevision: HealthPlanRevisionType = { + id: contractRev.id, + unlockInfo: contractRev.unlockInfo, + submitInfo: contractRev.submitInfo, + createdAt: contractRev.createdAt, + formDataProto, + } + + healthPlanRevisions.push(healthPlanRevision) + } + + return healthPlanRevisions +} + export { packageCurrentRevision, packageStatus, packageSubmittedAt, packageSubmitters, + convertContractToUnlockedHealthPlanPackage, } diff --git a/services/app-api/src/domain-models/index.ts b/services/app-api/src/domain-models/index.ts index d13cbfe951..a911f6aff2 100644 --- a/services/app-api/src/domain-models/index.ts +++ b/services/app-api/src/domain-models/index.ts @@ -23,6 +23,7 @@ export { packageStatus, packageSubmittedAt, packageSubmitters, + convertContractToUnlockedHealthPlanPackage, } from './healthPlanPackage' export type { diff --git a/services/app-api/src/postgres/contractAndRates/insertContract.ts b/services/app-api/src/postgres/contractAndRates/insertContract.ts index 7380dd2652..f3d477e4f5 100644 --- a/services/app-api/src/postgres/contractAndRates/insertContract.ts +++ b/services/app-api/src/postgres/contractAndRates/insertContract.ts @@ -10,9 +10,9 @@ import { draftContractRevisionsWithDraftRates } from '../prismaHelpers' type InsertContractArgsType = { stateCode: string - populationCovered: PopulationCoverageType + populationCovered?: PopulationCoverageType programIDs: string[] - riskBasedContract: boolean + riskBasedContract?: boolean submissionType: SubmissionType submissionDescription: string contractType: PrismaContractType diff --git a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts b/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts index 6e069d8ab3..43e66f8b0c 100644 --- a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts +++ b/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts @@ -20,6 +20,7 @@ describe('insertHealthPlanPackage', () => { submissionType: 'CONTRACT_ONLY' as const, submissionDescription: 'concurrency state code test', contractType: 'BASE' as const, + populationCovered: 'MEDICAID' as const, } const resultPromises = [] diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 6ab3e9b98d..a5defc4649 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -43,6 +43,11 @@ import { insertQuestionResponse, } from './questionResponse' import { findAllSupportedStates } from './state' +import { ContractType } from '../domain-models/contractAndRates/contractAndRatesZodSchema' +import { + InsertContractArgsType, + insertDraftContract, +} from './contractAndRates/insertContract' type Store = { findPrograms: ( @@ -116,6 +121,10 @@ type Store = { questionInput: InsertQuestionResponseArgs, user: StateUserType ) => Promise + + insertDraftContract: ( + args: InsertContractArgsType + ) => Promise } function NewPostgresStore(client: PrismaClient): Store { @@ -170,6 +179,10 @@ function NewPostgresStore(client: PrismaClient): Store { findAllQuestionsByHealthPlanPackage(client, pkgID), insertQuestionResponse: (questionInput, user) => insertQuestionResponse(client, questionInput, user), + /** + * Rates database refactor prisma functions + */ + insertDraftContract: (args) => insertDraftContract(client, args), } } diff --git a/services/app-api/src/resolvers/configureResolvers.ts b/services/app-api/src/resolvers/configureResolvers.ts index dedee3e724..cf9a6d4194 100644 --- a/services/app-api/src/resolvers/configureResolvers.ts +++ b/services/app-api/src/resolvers/configureResolvers.ts @@ -49,7 +49,10 @@ export function configureResolvers( ), }, Mutation: { - createHealthPlanPackage: createHealthPlanPackageResolver(store), + createHealthPlanPackage: createHealthPlanPackageResolver( + store, + launchDarkly + ), updateHealthPlanFormData: updateHealthPlanFormDataResolver(store), submitHealthPlanPackage: submitHealthPlanPackageResolver( store, diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts index 2355f09487..289fda662e 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts @@ -3,86 +3,118 @@ import CREATE_HEALTH_PLAN_PACKAGE from '../../../../app-graphql/src/mutations/cr import { constructTestPostgresServer } from '../../testHelpers/gqlHelpers' import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { testCMSUser } from '../../testHelpers/userHelpers' +import { testLDService } from '../../testHelpers/launchDarklyHelpers' +import { + FeatureFlagLDConstant, + FlagValue, +} from '../../../../app-web/src/common-code/featureFlags' -describe('createHealthPlanPackage', () => { - it('returns package with unlocked form data', async () => { - const server = await constructTestPostgresServer() +const flagValueTestParameters: { + flagName: FeatureFlagLDConstant + flagValue: FlagValue + testName: string +}[] = [ + { + flagName: 'rates-db-refactor', + flagValue: false, + testName: 'createHealthPlanPackage with all feature flags off', + }, + { + flagName: 'rates-db-refactor', + flagValue: true, + testName: 'createHealthPlanPackage with rates-db-refactor on', + }, +] - const input: CreateHealthPlanPackageInput = { - programIDs: [ - '5c10fe9f-bec9-416f-a20c-718b152ad633', - '037af66b-81eb-4472-8b80-01edf17d12d9', - ], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) +describe.each(flagValueTestParameters)( + `Tests $testName`, + ({ flagName, flagValue }) => { + const mockLDService = testLDService({ [flagName]: flagValue }) - expect(res.errors).toBeUndefined() + it('returns package with unlocked form data', async () => { + const server = await constructTestPostgresServer({ + ldService: mockLDService, + }) - const pkg = res.data?.createHealthPlanPackage.pkg - const draft = latestFormData(pkg) + const input: CreateHealthPlanPackageInput = { + programIDs: [ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + '037af66b-81eb-4472-8b80-01edf17d12d9', + ], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - expect(draft.submissionDescription).toBe('A real submission') - expect(draft.submissionType).toBe('CONTRACT_ONLY') - expect(draft.programIDs).toEqual([ - '5c10fe9f-bec9-416f-a20c-718b152ad633', - '037af66b-81eb-4472-8b80-01edf17d12d9', - ]) - expect(draft.documents).toHaveLength(0) - expect(draft.managedCareEntities).toHaveLength(0) - expect(draft.federalAuthorities).toHaveLength(0) - expect(draft.contractDateStart).toBeUndefined() - expect(draft.contractDateEnd).toBeUndefined() - }) + expect(res.errors).toBeUndefined() - it('returns an error if the program id is not in valid', async () => { - const server = await constructTestPostgresServer() - const input: CreateHealthPlanPackageInput = { - programIDs: ['xyz123'], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, + const pkg = res.data?.createHealthPlanPackage.pkg + const draft = latestFormData(pkg) + + expect(draft.submissionDescription).toBe('A real submission') + expect(draft.submissionType).toBe('CONTRACT_ONLY') + expect(draft.programIDs).toEqual([ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + '037af66b-81eb-4472-8b80-01edf17d12d9', + ]) + expect(draft.documents).toHaveLength(0) + expect(draft.managedCareEntities).toHaveLength(0) + expect(draft.federalAuthorities).toHaveLength(0) + expect(draft.contractDateStart).toBeUndefined() + expect(draft.contractDateEnd).toBeUndefined() }) - expect(res.errors).toBeDefined() - expect(res.errors && res.errors[0].message).toBe( - 'The program id xyz123 does not exist in state FL' - ) - }) + it('returns an error if the program id is not in valid', async () => { + const server = await constructTestPostgresServer({ + ldService: mockLDService, + }) + const input: CreateHealthPlanPackageInput = { + programIDs: ['xyz123'], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - it('returns an error if a CMS user attempts to create', async () => { - const server = await constructTestPostgresServer({ - context: { - user: testCMSUser(), - }, + expect(res.errors).toBeDefined() + expect(res.errors && res.errors[0].message).toBe( + 'The program id xyz123 does not exist in state FL' + ) }) - const input: CreateHealthPlanPackageInput = { - programIDs: ['xyz123'], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + it('returns an error if a CMS user attempts to create', async () => { + const server = await constructTestPostgresServer({ + context: { + user: testCMSUser(), + }, + ldService: mockLDService, + }) - expect(res.errors).toBeDefined() - expect(res.errors && res.errors[0].message).toBe( - 'user not authorized to create state data' - ) - }) -}) + const input: CreateHealthPlanPackageInput = { + programIDs: ['xyz123'], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) + + expect(res.errors).toBeDefined() + expect(res.errors && res.errors[0].message).toBe( + 'user not authorized to create state data' + ) + }) + } +) diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts index 8463c992e9..10f5ebfcc2 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts @@ -14,14 +14,22 @@ import { setSuccessAttributesOnActiveSpan, } from '../attributeHelper' import { GraphQLError } from 'graphql/index' +import { LDService } from '../../launchDarkly/launchDarkly' +import { convertContractToUnlockedHealthPlanPackage } from '../../domain-models' export function createHealthPlanPackageResolver( - store: Store + store: Store, + launchDarkly: LDService ): MutationResolvers['createHealthPlanPackage'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('createHealthPlanPackage', user, span) + const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( + context, + 'rates-db-refactor' + ) + // This resolver is only callable by state users if (!isStateUser(user)) { logError( @@ -71,24 +79,63 @@ export function createHealthPlanPackageResolver( contractType: input.contractType, } - const pkgResult = await store.insertHealthPlanPackage(insertArgs) - if (isStoreError(pkgResult)) { - const errMessage = `Error creating a package of type ${pkgResult.code}. Message: ${pkgResult.message}` - logError('createHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + //Here is where we flag the insert + if (ratesDatabaseRefactor) { + const contractResult = await store.insertDraftContract(insertArgs) + if (contractResult instanceof Error) { + const errMessage = `Error creating a draft contract. Message: ${contractResult.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + // Now we do the conversions + const pkg = + convertContractToUnlockedHealthPlanPackage(contractResult) + + if (pkg instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${pkg.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } + + logSuccess('createHealthPlanPackage') + setSuccessAttributesOnActiveSpan(span) + + return { + pkg, + } + } else { + const pkgResult = await store.insertHealthPlanPackage(insertArgs) + if (isStoreError(pkgResult)) { + const errMessage = `Error creating a package of type ${pkgResult.code}. Message: ${pkgResult.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - logSuccess('createHealthPlanPackage') - setSuccessAttributesOnActiveSpan(span) + logSuccess('createHealthPlanPackage') + setSuccessAttributesOnActiveSpan(span) - return { - pkg: pkgResult, + return { + pkg: pkgResult, + } } } } diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index d69f61e39b..fc3811c7a2 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -96,6 +96,11 @@ function mockStoreThatErrors(): Store { insertQuestionResponse: async (_ID) => { return genericStoreError }, + insertDraftContract: async (_ID) => { + return new Error( + 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' + ) + }, } } diff --git a/services/app-web/src/common-code/featureFlags/flags.ts b/services/app-web/src/common-code/featureFlags/flags.ts index a33e723ca0..122f915ea7 100644 --- a/services/app-web/src/common-code/featureFlags/flags.ts +++ b/services/app-web/src/common-code/featureFlags/flags.ts @@ -69,6 +69,13 @@ const featureFlags = { flag: 'supporting-docs-by-rate', defaultValue: false, }, + /** + * Rates refactor database handlers live behind this flag. We will use this to switchover to the new database tables when we migrate. + */ + RATES_DATABASE_REFACTOR: { + flag: 'rates-db-refactor', + defaultValue: false + }, /** * Used in testing to simulate errors in fetching flag value. * This flag does not exist in LaunchDarkly dashboard so fetching this will return the defaultValue.