Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MCR-3772 MCR-3771 Edit and submit standalone rate #2238

Merged
merged 28 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a4e2445
Start to add RateDetailsV2
haworku Jan 29, 2024
0b73291
wip
haworku Jan 30, 2024
8b74cd0
wip
haworku Jan 30, 2024
492de9f
add flag to side nav
haworku Jan 30, 2024
a1120f8
wip
haworku Jan 30, 2024
d8388bf
Add nx to gitignore
haworku Jan 31, 2024
d661aa9
Fix up types
haworku Jan 31, 2024
0a01796
Workaround actuary contacts types
haworku Feb 1, 2024
9423fc2
Fix React console error about bad setState call in RateSummary
haworku Feb 1, 2024
9ff2174
Wip to show what I'm doing
haworku Feb 1, 2024
d711b76
Page finally loading - can see a few broken fields to address next
haworku Feb 2, 2024
5c021ff
cleanup
haworku Feb 2, 2024
bb20c3d
Set up page action buttons properly, updateRate not available yet
haworku Feb 6, 2024
e8f23fd
cleanup
haworku Feb 6, 2024
3c3f417
cleanup
haworku Feb 6, 2024
860ea5a
Pass rateID not revision id to submit. Fix cacheing
haworku Feb 6, 2024
6dad58b
Move file nesting to combine all V2 in one folder
haworku Feb 6, 2024
9a6e986
Add basic test and todos for all the untested paths
haworku Feb 6, 2024
93997e6
Merge remote-tracking branch 'origin/main' into MCR-3777-single-rate
haworku Feb 6, 2024
6b20930
Cleanup app-web tests
haworku Feb 6, 2024
fa8f498
app-api tests passing
haworku Feb 6, 2024
a1b3769
Merge remote-tracking branch 'origin/main' into MCR-3777-single-rate
haworku Feb 6, 2024
13c4d03
cypress re-run
haworku Feb 6, 2024
0dfda94
Fix test id and help cypress out
haworku Feb 6, 2024
cc64ae6
Address @macrael code review
haworku Feb 9, 2024
7a8a24d
Think I can remove null-coalescing work now as well
haworku Feb 9, 2024
24c9f5a
Start generating rate names for standalone rate submits
haworku Feb 12, 2024
e4d0c01
Fixup app-api tests, don't mess with id-only submit log path
haworku Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

@haworku haworku Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@macrael I looked into changing graphql return types to be more generous via codegen settings first in the maybeValue but I was still seeing type issues. Feels like this could be something to look at more holistically. For now, doing this hack again (what we did in toDomain with protos) to quickly get around it in order to move on

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>(
haworku marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor Author

@haworku haworku Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the name on this because I was seeing types getting confused in my editor when RateType was both

  1. the enum for a rate's type (radio button response)
  2. the complex data object coming back from apollo client representing the entire rate (the full data model).

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
Loading