Skip to content

Commit

Permalink
MCR-3772 MCR-3771 Edit and submit standalone rate (#2238)
Browse files Browse the repository at this point in the history
  • Loading branch information
haworku authored Feb 12, 2024
1 parent 7c19422 commit e7f0999
Show file tree
Hide file tree
Showing 33 changed files with 1,478 additions and 140 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ tsconfig.tsbuildinfo
.serverless
.eslintcache
/.env
.nx
tests_output
*.log
coverage/
Expand Down
37 changes: 37 additions & 0 deletions services/app-api/src/domain-models/nullstoUndefined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Recursively replaces all nulls with undefineds
GQL return <Maybe> types are T | null instead of T | undefined which match our zod .optional() domain types
This functions allows us convert GQL to zod-friendly types to type match zod and apollo server types
and avoid manual type casting or null coalescing work
Adapted from https://github.com/apollographql/apollo-client/issues/2412
*/

type RecursivelyReplaceNullWithUndefined<T> = T extends null
? undefined
: T extends Date
? T
: {
[K in keyof T]: T[K] extends (infer U)[]
? RecursivelyReplaceNullWithUndefined<U>[]
: RecursivelyReplaceNullWithUndefined<T[K]>
}

export function nullsToUndefined<T>(
obj: T
): RecursivelyReplaceNullWithUndefined<T> {
if (obj === null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined as any
}

// object check based on: https://stackoverflow.com/a/51458052/6489012
if (obj?.constructor.name === 'Object') {
for (const key in obj) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj[key] = nullsToUndefined(obj[key]) as any
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return obj as any
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { formatRateNameDate } from '../../../../app-web/src/common-code/dateHelpers'
import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType'
import type { ProgramArgType } from '../../../../app-web/src/common-code/healthPlanFormDataType'
import type { RateFormDataType } from '../../domain-models/contractAndRates'

const generateRateCertificationName = (
rateFormData: RateFormDataType,
stateCode: string,
stateNumber: number,
statePrograms: ProgramArgType[]
): string => {
const {
rateType,
rateProgramIDs,
amendmentEffectiveDateEnd,
amendmentEffectiveDateStart,
rateDateCertified,
rateDateEnd,
rateDateStart,
} = rateFormData

let rateName = `${packageName(
stateCode,
stateNumber,
rateProgramIDs ?? [],
statePrograms
)}-RATE`
if (rateType === 'NEW' && rateDateStart) {
rateName = rateName.concat(
'-',
formatRateNameDate(rateDateStart),
'-',
formatRateNameDate(rateDateEnd),
'-',
'CERTIFICATION'
)
}

if (rateType === 'AMENDMENT') {
rateName = rateName.concat(
'-',
formatRateNameDate(amendmentEffectiveDateStart),
'-',
formatRateNameDate(amendmentEffectiveDateEnd),
'-',
'AMENDMENT'
)
}

if (rateDateCertified) {
rateName = rateName.concat('-', formatRateNameDate(rateDateCertified))
}
return rateName
}

export { generateRateCertificationName }
43 changes: 36 additions & 7 deletions services/app-api/src/resolvers/rate/submitRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import {
setErrorAttributesOnActiveSpan,
setResolverDetailsOnActiveSpan,
} from '../attributeHelper'
import type { RateFormDataType } from '../../domain-models'
import { isStateUser } from '../../domain-models'
import { logError } from '../../logger'
import { ForbiddenError, UserInputError } from 'apollo-server-lambda'
import { NotFoundError } from '../../postgres'
import { GraphQLError } from 'graphql/index'
import type { LDService } from '../../launchDarkly/launchDarkly'
import { generateRateCertificationName } from './generateRateCertificationName'
import { findStatePrograms } from '../../../../app-web/src/common-code/healthPlanFormDataType/findStatePrograms'
import { nullsToUndefined } from '../../domain-models/nullstoUndefined'

/*
Submit rate will change a draft revision to submitted and generate a rate name if one is missing
Also, if form data is passed in (such as on standalone rate edits) the form data itself will be updated
*/
export function submitRate(
store: Store,
launchDarkly: LDService
Expand Down Expand Up @@ -86,15 +92,33 @@ export function submitRate(
})
}

// prepare to generate rate cert name - either use new form data coming down on submit or unsubmitted submission data already in database
const stateCode = unsubmittedRate.stateCode
const stateNumber = unsubmittedRate.stateNumber
const statePrograms = findStatePrograms(stateCode)
const generatedRateCertName = formData
? generateRateCertificationName(
nullsToUndefined(formData),
stateCode,
stateNumber,
statePrograms
)
: generateRateCertificationName(
draftRateRevision.formData,
stateCode,
stateNumber,
statePrograms
)

// combine existing db draft data with any form data added on submit
// call submit rate handler
const submittedRate = await store.submitRate({
rateID,
submittedByUserID: user.id,
submitReason,
submitReason: submitReason ?? 'Initial submission',
formData: formData
? {
rateType: (formData.rateType ??
undefined) as RateFormDataType['rateType'],
rateType: formData.rateType ?? undefined,
rateCapitationType:
formData.rateCapitationType ?? undefined,
rateDocuments: formData.rateDocuments ?? [],
Expand All @@ -108,8 +132,6 @@ export function submitRate(
amendmentEffectiveDateEnd:
formData.amendmentEffectiveDateEnd ?? undefined,
rateProgramIDs: formData.rateProgramIDs ?? [],
rateCertificationName:
formData.rateCertificationName ?? undefined,
certifyingActuaryContacts:
formData.certifyingActuaryContacts
? formData.certifyingActuaryContacts.map(
Expand Down Expand Up @@ -140,7 +162,14 @@ export function submitRate(
actuaryCommunicationPreference:
formData.actuaryCommunicationPreference ?? undefined,
packagesWithSharedRateCerts:
formData.packagesWithSharedRateCerts ?? [],
formData.packagesWithSharedRateCerts.map((pkg) => ({
packageName: pkg.packageName ?? undefined,
packageId: pkg.packageId ?? undefined,
packageStatus: pkg.packageStatus ?? undefined,
})),
rateCertificationName:
formData.rateCertificationName ??
generatedRateCertName,
}
: undefined,
})
Expand Down
2 changes: 2 additions & 0 deletions services/app-api/src/testHelpers/gqlHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ const createAndUpdateTestHealthPlanPackage = async (
rateProgramIDs: [ratePrograms.reverse()[0].id],
actuaryContacts: [
{
id: '123-abc',
name: 'test name',
titleRole: 'test title',
email: 'email@example.com',
Expand All @@ -275,6 +276,7 @@ const createAndUpdateTestHealthPlanPackage = async (
]
draft.addtlActuaryContacts = [
{
id: '123-addtl-abv',
name: 'test name',
titleRole: 'test title',
email: 'email@example.com',
Expand Down
2 changes: 2 additions & 0 deletions services/app-graphql/src/mutations/submitRate.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ mutation submitRate($input: SubmitRateInput!) {
rateProgramIDs,
rateCertificationName,
certifyingActuaryContacts {
id
name
titleRole
email
actuarialFirm
actuarialFirmOther
},
addtlActuaryContacts {
id
name
titleRole
email
Expand Down
2 changes: 2 additions & 0 deletions services/app-graphql/src/mutations/unlockRate.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ fragment rateRevisionFragment on RateRevision {
rateProgramIDs,
rateCertificationName,
certifyingActuaryContacts {
id
name
titleRole
email
actuarialFirm
actuarialFirmOther
},
addtlActuaryContacts {
id
name
titleRole
email
Expand Down
2 changes: 2 additions & 0 deletions services/app-graphql/src/queries/fetchRate.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ fragment rateRevisionFragment on RateRevision {
rateProgramIDs,
rateCertificationName,
certifyingActuaryContacts {
id
name
titleRole
email
actuarialFirm
actuarialFirmOther
},
addtlActuaryContacts {
id
name
titleRole
email
Expand Down
18 changes: 10 additions & 8 deletions services/app-graphql/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ type ContractFormData {
}

"Either new capitation rates (NEW) or updates to previously certified capitation rates (AMENDMENT)"
enum RateType {
enum RateAmendmentType {
NEW
AMENDMENT
}
Expand Down Expand Up @@ -1013,6 +1013,7 @@ enum ActuarialFirm {

"Contact information for the certifying or additional state actuary"
type ActuaryContact {
id: ID
name: String
titleRole: String
email: String
Expand All @@ -1022,6 +1023,7 @@ type ActuaryContact {

"Contact information input for the certifying or additional state actuary"
input ActuaryContactInput {
id: ID
name: String
titleRole: String
email: String
Expand All @@ -1037,13 +1039,13 @@ It's used as a part of RateFormData
type PackageWithSameRate {
packageName: String!
packageId: String!
packageStatus: String
packageStatus: HealthPlanPackageStatus
}

input PackageWithSameRateInput {
packageName: String!
packageId: String!
packageStatus: HealthPlanPackageStatus!
packageStatus: HealthPlanPackageStatus
}

"""
Expand All @@ -1056,7 +1058,7 @@ type RateFormData {
Refers to whether the state is submitting a brand new rate certification
or an amendment to an existing rate certification
"""
rateType: RateType
rateType: RateAmendmentType
"""
Can be 'RATE_CELL' or 'RATE_RANGE'
These values represent on what basis the capitation rate is actuarially sound
Expand Down Expand Up @@ -1263,7 +1265,7 @@ input RateFormDataInput {
Refers to whether the state is submitting a brand new rate certification
or an amendment to an existing rate certification
"""
rateType: RateType
rateType: RateAmendmentType
"""
Can be 'RATE_CELL' or 'RATE_RANGE'
These values represent on what basis the capitation rate is actuarially sound
Expand Down Expand Up @@ -1327,7 +1329,7 @@ input RateFormDataInput {
An array of additional ActuaryContacts
Each element includes the the name, title/role and email
"""
addtlActuaryContacts: [ActuaryContactInput!]!
addtlActuaryContacts: [ActuaryContactInput!]
"""
Is either OACT_TO_ACTUARY or OACT_TO_STATE
It specifies whether the state wants CMS to reach out to their actuaries
Expand All @@ -1345,8 +1347,8 @@ input RateFormDataInput {

input SubmitRateInput {
rateID: ID!
"User given submission description"
submitReason: String!
"User given submission description - defaults to Initial submission if left blank"
submitReason: String
"Rate related form data to be updated with submission"
formData: RateFormDataInput
}
1 change: 1 addition & 0 deletions services/app-proto/src/health_plan_form_data.proto
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ message Contact {
optional string name = 1;
optional string title_role = 2;
optional string email = 3;
optional string id = 4;
}

// ContractInfo subtypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const stateContactSchema = z.object({
})

const actuaryContactSchema = z.object({
id: z.string().optional(),
name: z.string().optional(),
titleRole: z.string().optional(),
email: z.string().optional(),
Expand Down
Loading

0 comments on commit e7f0999

Please sign in to comment.