Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…e-review into mcr-3547-add-view-mccrs-id
  • Loading branch information
pearl-truss committed Oct 20, 2023
2 parents 52d699d + b939263 commit f3eaa81
Show file tree
Hide file tree
Showing 35 changed files with 1,790 additions and 307 deletions.
18 changes: 16 additions & 2 deletions docs/technical-design/contract-rate-change-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,23 @@ At the Postgres Table level, draft revisions and submitted revisions live in the

The list of revisions returned from prisma is run through [Zod](https://zod.dev/) to return [domain mode types](../../services/app-api/src/domain-models/contractAndRates). This is initiated by the `*WithHistory` database functions. See [parseContractWithHistory](../../services/app-api/src/postgres/contractAndRates/parseContractWithHistory.ts) and [parseRateWithHistory](../../services/app-api/src/postgres/contractAndRates/parseRateWithHistory.ts).

#### Contract History
- `parseContractWithHistory` takes our prisma contract data and parses into our domain `ContractType`. In `ContractType` the `revisions` is an array of **contract** **revisions**; this is the contract history.
- `revisions` differs from `draftRevision` in the `ContractType`. The `draftRevision` is a singular revision that is not submitted and this data has no historical significance until it is submitted. Most of the data in this revision can be updated.
- Each **contract revision** in `revisions` is submitted and retains data at the time of the submission. These revision's data will never be updated to retain its historical integrity.
- An important note about the `rateRevisions` field in each contract revision in `revision`.
- Like contracts, rates also have **rate revisions** which are used to construct a rate history through submissions, but the purpose of `rateRevisions` on a contract revision is not for rate history.
- The purpose of `rateRevisions` is to retain the data of a rate linked to this contract revision at the time of submission.
- For that we need the single rate revision that was submitted at the time this contract revision was submitted.
- Here are some guidelines for each rate revision in `rateRevisions` of a contract revision.
- Each rate revision in `rateRevisions` is unique by rate id, meaning there will never be two rate revisions with the same rate id in `rateRevisions`
- Each rate revision is the latest submitted up till the contract revision submitted time.
- Like contract revision, rate revision is read only and cannot be updated to retain its historical integrity.


*Dev Note*: If the `draftRevision` field has a value and the `revisions` field is an empty array, we know the Contract or Rate we are looking at is an initial draft that has never been submitted.

## The link between contract and rates is versioned. That link is solidified on submit.
### The link between contract and rates is versioned. That link is solidified on submit.

It's possible to tell if a link between a contract and rate has become outdated by refencing the `valid After` and `validUntil` fields on the join table between contract and rate revisions. The `validFrom` is set when a link is created (when a contract is submitted with a link to rates). At the point of creation, the `validUntil` is still null.

Expand All @@ -50,4 +64,4 @@ The `unlockInfo` and `submitInfo` associated with that revision is important met
When constructing a new package with a draft contract that has a draft rate, one of them must be submitted first. We have chosen that rates submit first, then contracts are submitted with a relationship to a set of submitted rates.

## Related documentation
- [Contract and Rates Refactor Relationships](./contract-rate-refactor-relationships.md).
- [Contract and Rates Refactor Relationships](./contract-rate-refactor-relationships.md).
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ const contractRevisionSchema = z.object({

const rateRevisionSchema = z.object({
id: z.string().uuid(),
// rateID: z.string(), // TODO we have this data in prisma but we lose it in the domain type - its needed for frontend which uses parent ids for routing
rate: z.object({
id: z.string().uuid(),
stateCode: z.string(),
stateNumber: z.number().min(1),
createdAt: z.date(),
}),
submitInfo: updateInfoSchema.optional(),
unlockInfo: updateInfoSchema.optional(),
createdAt: z.date(),
Expand Down
5 changes: 4 additions & 1 deletion services/app-api/src/handlers/proto_to_db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
unlockTestHealthPlanPackage,
updateTestHealthPlanFormData,
} from '../testHelpers/gqlHelpers'
import { v4 as uuidv4 } from 'uuid'
import { latestFormData } from '../testHelpers/healthPlanPackageHelpers'
import { testLDService } from '../testHelpers/launchDarklyHelpers'
import { testCMSUser } from '../testHelpers/userHelpers'
Expand All @@ -33,7 +34,7 @@ import type {
LockedHealthPlanFormDataType,
} from '../../../app-web/src/common-code/healthPlanFormDataType'

describe('test that we migrate things', () => {
describe.skip('test that we migrate things', () => {
const mockPreRefactorLDService = testLDService({
'rates-db-refactor': false,
})
Expand Down Expand Up @@ -99,6 +100,7 @@ describe('test that we migrate things', () => {

formData.rateInfos.push(
{
id: uuidv4(),
rateDateStart: new Date(),
rateDateEnd: new Date(),
rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'],
Expand All @@ -123,6 +125,7 @@ describe('test that we migrate things', () => {
],
},
{
id: uuidv4(),
rateDateStart: new Date(),
rateDateEnd: new Date(),
rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ describe('parseDomainData', () => {
isRemoval: false,
rateRevision: {
id: uuidv4(),
rate: {
id: '24fb2a5f-6d0d-4e26-9906-4de28927c882',
createdAt: new Date(),
updatedAt: new Date(),
stateCode: 'MN',
stateNumber: 111,
},
rateID: 'Rate ID',
createdAt: new Date(),
updatedAt: new Date(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,36 +194,31 @@ function contractWithHistoryToDomainModel(
// }
// }

// Basically the same as above, except we do not create new contract revisions for rate changes.
// This loop is finding the rate revision submitted along with this contract revision to preserve the historical
// rate data for the submission history.
for (const rateRev of contractRev.rateRevisions) {
if (!rateRev.rateRevision.submitInfo) {
return new Error(
'Programming Error: a contract is associated with an unsubmitted rate'
)
}

// if it's from before this contract was submitted, it's there at the beginning.
// Make sure this rate revision was not submitted after this contract revision, and it was not removed.
if (
rateRev.rateRevision.submitInfo.updatedAt <=
contractRev.submitInfo.updatedAt
contractRev.submitInfo.updatedAt &&
!rateRev.isRemoval
) {
if (!rateRev.isRemoval) {
initialEntry.rateRevisions.push(rateRev.rateRevision)
}
} else {
// if after, then it's always a new entry in the list
let lastRates = [...initialEntry.rateRevisions]

// take out the previous rate revision this revision supersedes
lastRates = lastRates.filter(
(r) => r.rateID !== rateRev.rateRevision.rateID
const filteredRevisions = initialEntry.rateRevisions.filter(
(rr) => rr.rateID !== rateRev.rateRevision.rateID
)
// an isRemoval entry indicates that this rate was removed from this contract.
if (!rateRev.isRemoval) {
lastRates.push(rateRev.rateRevision)
}

initialEntry.rateRevisions = lastRates
// add latest revision
filteredRevisions.push(rateRev.rateRevision)

// Sort to retain order by rate.createdAt.
initialEntry.rateRevisions = filteredRevisions
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function rateRevisionToDomainModel(

return {
id: revision.id,
rate: revision.rate,
createdAt: revision.createdAt,
updatedAt: revision.updatedAt,
submitInfo: convertUpdateInfoToDomainModel(revision.submitInfo),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function draftRateRevToDomainModel(

return {
id: revision.id,
rate: revision.rate,
createdAt: revision.createdAt,
updatedAt: revision.updatedAt,
formData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function getContractRateStatus(
const includeRateFormData = {
submitInfo: includeUpdateInfo,
unlockInfo: includeUpdateInfo,
rate: true,

rateDocuments: {
orderBy: {
Expand Down Expand Up @@ -224,6 +225,7 @@ function rateRevisionToDomainModel(

return {
id: revision.id,
rate: revision.rate,
createdAt: revision.createdAt,
updatedAt: revision.updatedAt,
unlockInfo: convertUpdateInfoToDomainModel(revision.unlockInfo),
Expand All @@ -248,7 +250,7 @@ function ratesRevisionsToDomainModel(
}

domainRevisions.sort(
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
(a, b) => a.rate.createdAt.getTime() - b.rate.createdAt.getTime()
)

return domainRevisions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export async function cleanupLastMigration(
client.rateRevisionsOnContractRevisionsTable.deleteMany(),
client.contractRevisionTable.deleteMany(),
client.rateRevisionTable.deleteMany(),
client.rateRevisionsOnContractRevisionsTable.deleteMany(),
client.updateInfoTable.deleteMany(),

// must be last due to foreign keys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ describe('unlockContract', () => {
)
})

it('Unlocks a rate without breaking connected submitted contract', async () => {
// This is unlocking a rate without unlocking the contract that this rate belongs to. Then it updates the rate and resubmits.
// The rate gets a new revision, but the submitted contract does not.
// This test does not simulate how creating/updating a rate currently works in our app and the contract revision history
// will not match.
// Skipping this for now, revisit during rate only feature work.
it.skip('Unlocks a rate without breaking connected submitted contract', async () => {
const client = await sharedTestPrismaClient()

const stateUser = await client.user.create({
Expand Down
Loading

0 comments on commit f3eaa81

Please sign in to comment.