-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MCR-3767 CMS has unlock rate button (#2166)
* Initial backend commit * initial frontend commit * Get button working and cleanup unused scss * Add tests * and add the feature flag * Finish app-api tests, add assertionHelpers * Fix padding by moving dl * Fix word wrap in rate names with non zero space * Revert "Fix word wrap in rate names with non zero space" This reverts commit cf47ef1. * Cleanup from code review
- Loading branch information
Showing
18 changed files
with
645 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { | ||
constructTestPostgresServer, | ||
createAndSubmitTestHealthPlanPackage, | ||
} from '../../testHelpers/gqlHelpers' | ||
import UNLOCK_RATE from '../../../../app-graphql/src/mutations/unlockRate.graphql' | ||
import { testCMSUser } from '../../testHelpers/userHelpers' | ||
import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' | ||
import { expectToBeDefined } from '../../testHelpers/assertionHelpers' | ||
|
||
describe(`unlockRate`, () => { | ||
const cmsUser = testCMSUser() | ||
|
||
it('changes rate status to UNLOCKED and creates a new draft revision with unlock info', async () => { | ||
const stateServer = await constructTestPostgresServer() | ||
const cmsServer = await constructTestPostgresServer({ | ||
context: { | ||
user: cmsUser, | ||
}, | ||
}) | ||
|
||
// Create a rate | ||
const submission = | ||
await createAndSubmitTestHealthPlanPackage(stateServer) | ||
const initialSubmitFormData = latestFormData(submission) | ||
const rateID = initialSubmitFormData.rateInfos[0].id | ||
expect(rateID).toBeDefined() | ||
|
||
// unlock rate | ||
const unlockedReason = 'Super duper good reason.' | ||
const unlockResult = await cmsServer.executeOperation({ | ||
query: UNLOCK_RATE, | ||
variables: { | ||
input: { | ||
rateID, | ||
unlockedReason, | ||
}, | ||
}, | ||
}) | ||
|
||
expect(unlockResult.errors).toBeUndefined() | ||
|
||
const updatedRate = unlockResult.data?.unlockRate.rate | ||
|
||
console.info(updatedRate) | ||
expect(updatedRate.status).toBe('UNLOCKED') | ||
expect(updatedRate.draftRevision).toBeDefined() | ||
expect(updatedRate.draftRevision.unlockInfo.updatedReason).toEqual( | ||
unlockedReason | ||
) | ||
}) | ||
|
||
it('returns status error if rate is actively being edited in draft', async () => { | ||
const stateServer = await constructTestPostgresServer() | ||
const cmsServer = await constructTestPostgresServer({ | ||
context: { | ||
user: cmsUser, | ||
}, | ||
}) | ||
|
||
// Create a rate | ||
const submission = | ||
await createAndSubmitTestHealthPlanPackage(stateServer) | ||
const initialSubmitFormData = latestFormData(submission) | ||
const targetRateBeforeUnlock = initialSubmitFormData.rateInfos[0] | ||
expectToBeDefined(targetRateBeforeUnlock.id) | ||
|
||
// Unlock the rate once | ||
const unlockResult1 = await cmsServer.executeOperation({ | ||
query: UNLOCK_RATE, | ||
variables: { | ||
input: { | ||
rateID: targetRateBeforeUnlock.id, | ||
unlockedReason: 'Super duper good reason.', | ||
}, | ||
}, | ||
}) | ||
|
||
expect(unlockResult1.errors).toBeUndefined() | ||
|
||
// Try to unlock the rate again | ||
const unlockResult2 = await cmsServer.executeOperation({ | ||
query: UNLOCK_RATE, | ||
variables: { | ||
input: { | ||
rateID: targetRateBeforeUnlock.id, | ||
unlockedReason: 'Super duper good reason.', | ||
}, | ||
}, | ||
}) | ||
|
||
expectToBeDefined(unlockResult2.errors) | ||
expect(unlockResult2.errors[0].message).toBe( | ||
'Attempted to unlock rate with wrong status' | ||
) | ||
}) | ||
|
||
it('returns unauthorized error for state user', async () => { | ||
const stateServer = await constructTestPostgresServer() | ||
// Create a rate | ||
const submission = | ||
await createAndSubmitTestHealthPlanPackage(stateServer) | ||
const initialSubmitFormData = latestFormData(submission) | ||
const targetRateBeforeUnlock = initialSubmitFormData.rateInfos[0] | ||
expect(targetRateBeforeUnlock.id).toBeDefined() | ||
|
||
// Unlock the rate | ||
const unlockResult = await stateServer.executeOperation({ | ||
query: UNLOCK_RATE, | ||
variables: { | ||
input: { | ||
rateID: targetRateBeforeUnlock.id, | ||
unlockedReason: 'Super duper good reason.', | ||
}, | ||
}, | ||
}) | ||
|
||
expectToBeDefined(unlockResult.errors) | ||
expect(unlockResult.errors[0].message).toBe( | ||
'user not authorized to unlock rate' | ||
) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { ForbiddenError, UserInputError } from 'apollo-server-lambda' | ||
import type { RateType } from '../../domain-models' | ||
import { isCMSUser } from '../../domain-models' | ||
import type { MutationResolvers } from '../../gen/gqlServer' | ||
import { logError, logSuccess } from '../../logger' | ||
import { NotFoundError } from '../../postgres' | ||
import type { Store } from '../../postgres' | ||
import { | ||
setErrorAttributesOnActiveSpan, | ||
setResolverDetailsOnActiveSpan, | ||
setSuccessAttributesOnActiveSpan, | ||
} from '../attributeHelper' | ||
import { GraphQLError } from 'graphql' | ||
|
||
export function unlockRate(store: Store): MutationResolvers['unlockRate'] { | ||
return async (_parent, { input }, context) => { | ||
const { user, span } = context | ||
const { unlockedReason, rateID } = input | ||
setResolverDetailsOnActiveSpan('unlockRate', user, span) | ||
span?.setAttribute('mcreview.rate_id', rateID) | ||
|
||
// This resolver is only callable by CMS users | ||
if (!isCMSUser(user)) { | ||
logError('unlockRate', 'user not authorized to unlock rate') | ||
setErrorAttributesOnActiveSpan( | ||
'user not authorized to unlock rate', | ||
span | ||
) | ||
throw new ForbiddenError('user not authorized to unlock rate') | ||
} | ||
|
||
const initialRateResult = await store.findRateWithHistory(rateID) | ||
|
||
if (initialRateResult instanceof Error) { | ||
if (initialRateResult instanceof NotFoundError) { | ||
const errMessage = `A rate must exist to be unlocked: ${rateID}` | ||
logError('unlockRate', errMessage) | ||
setErrorAttributesOnActiveSpan(errMessage, span) | ||
throw new UserInputError(errMessage, { | ||
argumentName: 'rateID', | ||
}) | ||
} | ||
|
||
const errMessage = `Issue finding a rate. Message: ${initialRateResult.message}` | ||
logError('unlockRate', errMessage) | ||
setErrorAttributesOnActiveSpan(errMessage, span) | ||
throw new GraphQLError(errMessage, { | ||
extensions: { | ||
code: 'INTERNAL_SERVER_ERROR', | ||
cause: 'DB_ERROR', | ||
}, | ||
}) | ||
} | ||
|
||
const rate: RateType = initialRateResult | ||
|
||
if (rate.draftRevision) { | ||
const errMessage = `Attempted to unlock rate with wrong status` | ||
logError('unlockRate', errMessage) | ||
setErrorAttributesOnActiveSpan(errMessage, span) | ||
throw new UserInputError(errMessage, { | ||
argumentName: 'rateID', | ||
cause: 'INVALID_PACKAGE_STATUS', | ||
}) | ||
} | ||
|
||
const unlockRateResult = await store.unlockRate({ | ||
rateID: rate.id, | ||
unlockReason: unlockedReason, | ||
unlockedByUserID: user.id, | ||
}) | ||
|
||
if (unlockRateResult instanceof Error) { | ||
const errMessage = `Failed to unlock rate revision with ID: ${rate.id}; ${unlockRateResult.message}` | ||
logError('unlockRate', errMessage) | ||
setErrorAttributesOnActiveSpan(errMessage, span) | ||
throw new GraphQLError(errMessage, { | ||
extensions: { | ||
code: 'INTERNAL_SERVER_ERROR', | ||
cause: 'DB_ERROR', | ||
}, | ||
}) | ||
} | ||
|
||
logSuccess('unlockRate') | ||
setSuccessAttributesOnActiveSpan(span) | ||
|
||
return { rate: unlockRateResult } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Functions that assists with type narrow in jest tests | ||
|
||
// must - interrupts test flow to through an error | ||
function must<T>(maybeErr: T | Error): T { | ||
if (maybeErr instanceof Error) { | ||
throw maybeErr | ||
} | ||
return maybeErr | ||
} | ||
|
||
// expectToBeDefined - properly type narrows the results of toBeDefined | ||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/41179 would eventually address this but is not implemented | ||
function expectToBeDefined<T>(arg: T): asserts arg is NonNullable<T> { | ||
expect(arg).toBeDefined() | ||
} | ||
|
||
export { must, expectToBeDefined } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.