From bef7179c25bb18bc282dbb7defbd58efb4073458 Mon Sep 17 00:00:00 2001 From: Garrett Rabian Date: Mon, 16 Oct 2023 15:49:58 -0400 Subject: [PATCH 1/4] add pccm filtering --- .../src/components/reports/ReportProvider.tsx | 4 + services/ui-src/src/utils/reports/reports.ts | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/services/ui-src/src/components/reports/ReportProvider.tsx b/services/ui-src/src/components/reports/ReportProvider.tsx index 7f9802351..7e2205e34 100644 --- a/services/ui-src/src/components/reports/ReportProvider.tsx +++ b/services/ui-src/src/components/reports/ReportProvider.tsx @@ -6,6 +6,7 @@ import { releaseReport as releaseReportRequest, submitReport as submitReportRequest, flattenReportRoutesArray, + generatePCCMTemplate, getLocalHourMinuteTime, getReport, getReportsByState, @@ -68,6 +69,9 @@ export const ReportProvider = ({ children }: Props) => { const hydrateAndSetReport = (report: ReportShape | undefined) => { if (report) { + if (report?.programIsPCCM?.[0]?.value === "Yes") { + report.formTemplate = generatePCCMTemplate(report.formTemplate); + } report.formTemplate.flatRoutes = flattenReportRoutesArray( report.formTemplate.routes ); diff --git a/services/ui-src/src/utils/reports/reports.ts b/services/ui-src/src/utils/reports/reports.ts index c908efe44..a4330c84c 100644 --- a/services/ui-src/src/utils/reports/reports.ts +++ b/services/ui-src/src/utils/reports/reports.ts @@ -2,6 +2,7 @@ import { AnyObject, FieldChoice, FormField, + ReportJson, ReportShape, ReportRoute, isFieldElement, @@ -31,6 +32,111 @@ export const flattenReportRoutesArray = ( return routesArray; }; +// saving +// Section A: Program Information +// Point of Contact +// Reporting Period +// Add Plans +// Section B: State-Level Indicators +// Program Characteristics and Enrollment +// Section C: Program-Level Indicators +// Program Characteristics +// Section D: Plan-Level Indicators +// Program Characteristics and Enrollment +// Sanctions +// Review and Submit + +// entities to keep +// plans +// sanctions + +const routesToInclude = { + sectionA: ["Point of Contact", "Reporting Period", "Add Plans"], + sectionB: ["I: Program Characteristics"], + sectionC: ["I: Program Characteristics"], + sectionD: ["I: Program Characteristics", "VIII: Sanctions"], +}; +const entitiesToInclude = ["plans", "sanctions"]; + +export const generatePCCMTemplate = (reportTemplate: any) => { + let filteredEntities: any = {}; + + for (const key of entitiesToInclude) { + if (reportTemplate.entities[key]) { + filteredEntities[key] = reportTemplate.entities[key]; + } + } + + // Section A + const indexOfSectionA = reportTemplate.routes.findIndex((route: any) => + route.name.includes("A:") + ); + const filteredSectionAChildren: any = []; + for (const route of routesToInclude.sectionA) { + const indexOfSubSection = reportTemplate.routes[ + indexOfSectionA + ].children.findIndex((subSection: any) => subSection.name === route); + filteredSectionAChildren.push( + reportTemplate.routes[indexOfSectionA].children[indexOfSubSection] + ); + } + reportTemplate.routes[indexOfSectionA].children = filteredSectionAChildren; + + // Section B + const indexOfSectionB = reportTemplate.routes.findIndex((route: any) => + route.name.includes("B:") + ); + const filteredSectionBChildren: any = []; + for (const route of routesToInclude.sectionB) { + const indexOfSubSection = reportTemplate.routes[ + indexOfSectionB + ].children.findIndex((subSection: any) => subSection.name === route); + filteredSectionBChildren.push( + reportTemplate.routes[indexOfSectionB].children[indexOfSubSection] + ); + } + reportTemplate.routes[indexOfSectionB].children = filteredSectionBChildren; + + // Section C + const indexOfSectionC = reportTemplate.routes.findIndex((route: any) => + route.name.includes("C:") + ); + const filteredSectionCChildren: any = []; + for (const route of routesToInclude.sectionC) { + const indexOfSubSection = reportTemplate.routes[ + indexOfSectionC + ].children.findIndex((subSection: any) => subSection.name === route); + filteredSectionCChildren.push( + reportTemplate.routes[indexOfSectionC].children[indexOfSubSection] + ); + } + reportTemplate.routes[indexOfSectionC].children = filteredSectionCChildren; + + // Section D + const indexOfSectionD = reportTemplate.routes.findIndex((route: any) => + route.name.includes("D:") + ); + const filteredSectionDChildren: any = []; + for (const route of routesToInclude.sectionD) { + const indexOfSubSection = reportTemplate.routes[ + indexOfSectionD + ].children.findIndex((subSection: any) => subSection.name === route); + filteredSectionDChildren.push( + reportTemplate.routes[indexOfSectionD].children[indexOfSubSection] + ); + } + reportTemplate.routes[indexOfSectionD].children = filteredSectionDChildren; + + // Section E + const indexOfSectionE = reportTemplate.routes.findIndex((route: any) => + route.name.includes("E:") + ); + delete reportTemplate.routes[indexOfSectionE]; + + reportTemplate.entities = filteredEntities; + return reportTemplate; +}; + // returns validation schema object for array of fields export const compileValidationJsonFromFields = ( fieldArray: FormField[], From 18f800247114e7dc9171ff1dc379a6d38c457bf2 Mon Sep 17 00:00:00 2001 From: Garrett Rabian Date: Mon, 16 Oct 2023 16:25:48 -0400 Subject: [PATCH 2/4] remove unused import --- services/ui-src/src/utils/reports/reports.ts | 119 ++++--------------- 1 file changed, 26 insertions(+), 93 deletions(-) diff --git a/services/ui-src/src/utils/reports/reports.ts b/services/ui-src/src/utils/reports/reports.ts index a4330c84c..78814234c 100644 --- a/services/ui-src/src/utils/reports/reports.ts +++ b/services/ui-src/src/utils/reports/reports.ts @@ -2,7 +2,6 @@ import { AnyObject, FieldChoice, FormField, - ReportJson, ReportShape, ReportRoute, isFieldElement, @@ -32,108 +31,42 @@ export const flattenReportRoutesArray = ( return routesArray; }; -// saving -// Section A: Program Information -// Point of Contact -// Reporting Period -// Add Plans -// Section B: State-Level Indicators -// Program Characteristics and Enrollment -// Section C: Program-Level Indicators -// Program Characteristics -// Section D: Plan-Level Indicators -// Program Characteristics and Enrollment -// Sanctions -// Review and Submit - -// entities to keep -// plans -// sanctions - const routesToInclude = { - sectionA: ["Point of Contact", "Reporting Period", "Add Plans"], - sectionB: ["I: Program Characteristics"], - sectionC: ["I: Program Characteristics"], - sectionD: ["I: Program Characteristics", "VIII: Sanctions"], -}; + "A: Program Information": [ + "Point of Contact", + "Reporting Period", + "Add Plans", + ], + "B: State-Level Indicators": ["I: Program Characteristics"], + "C: Program-Level Indicators": ["I: Program Characteristics"], + "D: Plan-Level Indicators": ["I: Program Characteristics", "VIII: Sanctions"], + "Review & Submit": [], +} as { [key: string]: string[] }; + const entitiesToInclude = ["plans", "sanctions"]; export const generatePCCMTemplate = (reportTemplate: any) => { - let filteredEntities: any = {}; - - for (const key of entitiesToInclude) { - if (reportTemplate.entities[key]) { - filteredEntities[key] = reportTemplate.entities[key]; - } - } - - // Section A - const indexOfSectionA = reportTemplate.routes.findIndex((route: any) => - route.name.includes("A:") - ); - const filteredSectionAChildren: any = []; - for (const route of routesToInclude.sectionA) { - const indexOfSubSection = reportTemplate.routes[ - indexOfSectionA - ].children.findIndex((subSection: any) => subSection.name === route); - filteredSectionAChildren.push( - reportTemplate.routes[indexOfSectionA].children[indexOfSubSection] - ); - } - reportTemplate.routes[indexOfSectionA].children = filteredSectionAChildren; - - // Section B - const indexOfSectionB = reportTemplate.routes.findIndex((route: any) => - route.name.includes("B:") + // remove top level sections not in include list + reportTemplate.routes = reportTemplate.routes.filter( + (route: ReportRoute) => !!routesToInclude[route.name] ); - const filteredSectionBChildren: any = []; - for (const route of routesToInclude.sectionB) { - const indexOfSubSection = reportTemplate.routes[ - indexOfSectionB - ].children.findIndex((subSection: any) => subSection.name === route); - filteredSectionBChildren.push( - reportTemplate.routes[indexOfSectionB].children[indexOfSubSection] - ); - } - reportTemplate.routes[indexOfSectionB].children = filteredSectionBChildren; - // Section C - const indexOfSectionC = reportTemplate.routes.findIndex((route: any) => - route.name.includes("C:") - ); - const filteredSectionCChildren: any = []; - for (const route of routesToInclude.sectionC) { - const indexOfSubSection = reportTemplate.routes[ - indexOfSectionC - ].children.findIndex((subSection: any) => subSection.name === route); - filteredSectionCChildren.push( - reportTemplate.routes[indexOfSectionC].children[indexOfSubSection] - ); + // only include listed subsections + for (let route of reportTemplate.routes) { + if (route?.children) { + route.children = route.children.filter((childRoute: ReportRoute) => + routesToInclude[route.name].includes(childRoute.name) + ); + } } - reportTemplate.routes[indexOfSectionC].children = filteredSectionCChildren; - // Section D - const indexOfSectionD = reportTemplate.routes.findIndex((route: any) => - route.name.includes("D:") - ); - const filteredSectionDChildren: any = []; - for (const route of routesToInclude.sectionD) { - const indexOfSubSection = reportTemplate.routes[ - indexOfSectionD - ].children.findIndex((subSection: any) => subSection.name === route); - filteredSectionDChildren.push( - reportTemplate.routes[indexOfSectionD].children[indexOfSubSection] - ); + // Any entity not in the allow list must be removed. + for (let entityType of Object.keys(reportTemplate.entities)) { + if (!entitiesToInclude.includes(entityType)) { + delete reportTemplate.entities[entityType]; + } } - reportTemplate.routes[indexOfSectionD].children = filteredSectionDChildren; - - // Section E - const indexOfSectionE = reportTemplate.routes.findIndex((route: any) => - route.name.includes("E:") - ); - delete reportTemplate.routes[indexOfSectionE]; - reportTemplate.entities = filteredEntities; return reportTemplate; }; From ee4c3d43e24afbde6137fe20fbd6247ee3fb99c7 Mon Sep 17 00:00:00 2001 From: Garrett Rabian Date: Tue, 17 Oct 2023 10:23:54 -0400 Subject: [PATCH 3/4] generate pccm template in backend --- services/app-api/handlers/reports/create.ts | 3 +- .../utils/formTemplates/formTemplates.ts | 49 ++++++++++++++++++- .../src/components/reports/ReportProvider.tsx | 4 -- services/ui-src/src/utils/reports/reports.ts | 39 --------------- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/services/app-api/handlers/reports/create.ts b/services/app-api/handlers/reports/create.ts index b842e1e89..ca1d2466e 100644 --- a/services/app-api/handlers/reports/create.ts +++ b/services/app-api/handlers/reports/create.ts @@ -86,7 +86,8 @@ export const createReport = handler(async (event, _context) => { try { ({ formTemplate, formTemplateVersion } = await getOrCreateFormTemplate( reportBucket, - reportType + reportType, + unvalidatedMetadata )); } catch (err) { logger.error(err, "Error getting or creating template"); diff --git a/services/app-api/utils/formTemplates/formTemplates.ts b/services/app-api/utils/formTemplates/formTemplates.ts index 04f48350c..d9751f000 100644 --- a/services/app-api/utils/formTemplates/formTemplates.ts +++ b/services/app-api/utils/formTemplates/formTemplates.ts @@ -12,6 +12,7 @@ import { FormField, FormLayoutElement, FormTemplate, + MCPARReportMetadata, ModalOverlayReportPageShape, ReportJson, ReportRoute, @@ -54,9 +55,14 @@ export const formTemplateForReportType = (reportType: ReportType) => { export async function getOrCreateFormTemplate( reportBucket: string, - reportType: ReportType + reportType: ReportType, + metadata: MCPARReportMetadata ) { - const currentFormTemplate = formTemplateForReportType(reportType); + let currentFormTemplate = formTemplateForReportType(reportType); + // if program is PCCM generate shortened template + if (metadata?.programIsPCCM?.[0]?.value === "Yes") { + currentFormTemplate = generatePCCMTemplate(currentFormTemplate); + } const stringifiedTemplate = JSON.stringify(currentFormTemplate); const currentTemplateHash = createHash("md5") @@ -244,3 +250,42 @@ export function getValidationFromFormTemplate(reportJson: ReportJson) { export function getPossibleFieldsFromFormTemplate(reportJson: ReportJson) { return Object.keys(getValidationFromFormTemplate(reportJson)); } + +const routesToIncludeInPCCM = { + "A: Program Information": [ + "Point of Contact", + "Reporting Period", + "Add Plans", + ], + "B: State-Level Indicators": ["I: Program Characteristics"], + "C: Program-Level Indicators": ["I: Program Characteristics"], + "D: Plan-Level Indicators": ["I: Program Characteristics", "VIII: Sanctions"], + "Review & Submit": [], +} as { [key: string]: string[] }; + +const entitiesToIncludeInPCCM = ["plans", "sanctions"]; + +export const generatePCCMTemplate = (reportTemplate: any) => { + // remove top level sections not in include list + reportTemplate.routes = reportTemplate.routes.filter( + (route: ReportRoute) => !!routesToIncludeInPCCM[route.name] + ); + + // only include listed subsections + for (let route of reportTemplate.routes) { + if (route?.children) { + route.children = route.children.filter((childRoute: ReportRoute) => + routesToIncludeInPCCM[route.name].includes(childRoute.name) + ); + } + } + + // Any entity not in the allow list must be removed. + for (let entityType of Object.keys(reportTemplate.entities)) { + if (!entitiesToIncludeInPCCM.includes(entityType)) { + delete reportTemplate.entities[entityType]; + } + } + + return reportTemplate; +}; diff --git a/services/ui-src/src/components/reports/ReportProvider.tsx b/services/ui-src/src/components/reports/ReportProvider.tsx index 7e2205e34..7f9802351 100644 --- a/services/ui-src/src/components/reports/ReportProvider.tsx +++ b/services/ui-src/src/components/reports/ReportProvider.tsx @@ -6,7 +6,6 @@ import { releaseReport as releaseReportRequest, submitReport as submitReportRequest, flattenReportRoutesArray, - generatePCCMTemplate, getLocalHourMinuteTime, getReport, getReportsByState, @@ -69,9 +68,6 @@ export const ReportProvider = ({ children }: Props) => { const hydrateAndSetReport = (report: ReportShape | undefined) => { if (report) { - if (report?.programIsPCCM?.[0]?.value === "Yes") { - report.formTemplate = generatePCCMTemplate(report.formTemplate); - } report.formTemplate.flatRoutes = flattenReportRoutesArray( report.formTemplate.routes ); diff --git a/services/ui-src/src/utils/reports/reports.ts b/services/ui-src/src/utils/reports/reports.ts index 78814234c..c908efe44 100644 --- a/services/ui-src/src/utils/reports/reports.ts +++ b/services/ui-src/src/utils/reports/reports.ts @@ -31,45 +31,6 @@ export const flattenReportRoutesArray = ( return routesArray; }; -const routesToInclude = { - "A: Program Information": [ - "Point of Contact", - "Reporting Period", - "Add Plans", - ], - "B: State-Level Indicators": ["I: Program Characteristics"], - "C: Program-Level Indicators": ["I: Program Characteristics"], - "D: Plan-Level Indicators": ["I: Program Characteristics", "VIII: Sanctions"], - "Review & Submit": [], -} as { [key: string]: string[] }; - -const entitiesToInclude = ["plans", "sanctions"]; - -export const generatePCCMTemplate = (reportTemplate: any) => { - // remove top level sections not in include list - reportTemplate.routes = reportTemplate.routes.filter( - (route: ReportRoute) => !!routesToInclude[route.name] - ); - - // only include listed subsections - for (let route of reportTemplate.routes) { - if (route?.children) { - route.children = route.children.filter((childRoute: ReportRoute) => - routesToInclude[route.name].includes(childRoute.name) - ); - } - } - - // Any entity not in the allow list must be removed. - for (let entityType of Object.keys(reportTemplate.entities)) { - if (!entitiesToInclude.includes(entityType)) { - delete reportTemplate.entities[entityType]; - } - } - - return reportTemplate; -}; - // returns validation schema object for array of fields export const compileValidationJsonFromFields = ( fieldArray: FormField[], From d319bfcbd0cd5c4297d4c48493a1c32a88bed01a Mon Sep 17 00:00:00 2001 From: Garrett Rabian Date: Tue, 17 Oct 2023 15:05:32 -0400 Subject: [PATCH 4/4] refactor to use hash; add tests --- .../formTemplates/populateTemplatesTable.ts | 21 ----- services/app-api/handlers/reports/create.ts | 5 +- .../utils/formTemplates/formTemplates.test.ts | 93 +++++++++++++++---- .../utils/formTemplates/formTemplates.ts | 45 ++++++--- 4 files changed, 110 insertions(+), 54 deletions(-) diff --git a/services/app-api/handlers/formTemplates/populateTemplatesTable.ts b/services/app-api/handlers/formTemplates/populateTemplatesTable.ts index d72ded05d..acfc8ae5b 100644 --- a/services/app-api/handlers/formTemplates/populateTemplatesTable.ts +++ b/services/app-api/handlers/formTemplates/populateTemplatesTable.ts @@ -16,7 +16,6 @@ import { import { S3 } from "aws-sdk"; import * as path from "path"; import { logger } from "../../utils/logging"; -import { AttributeValue, QueryInput } from "aws-sdk/clients/dynamodb"; import { createHash } from "crypto"; type S3ObjectRequired = SomeRequired; @@ -34,26 +33,6 @@ type FormTemplateMetaData = { hash: string; }; -/** - * - * @param reportType report type - * @param hash hash to look for - * @returns - */ -export function getTemplateVersionByHash(reportType: ReportType, hash: string) { - const queryParams: QueryInput = { - TableName: process.env.FORM_TEMPLATE_TABLE_NAME!, - IndexName: "HashIndex", - KeyConditionExpression: "reportType = :reportType AND md5Hash = :md5Hash", - Limit: 1, - ExpressionAttributeValues: { - ":md5Hash": hash as AttributeValue, - ":reportType": reportType as unknown as AttributeValue, - }, - }; - return dynamodbLib.query(queryParams); -} - /** * Retrieve template data from S3 * diff --git a/services/app-api/handlers/reports/create.ts b/services/app-api/handlers/reports/create.ts index ca1d2466e..5ae572a7f 100644 --- a/services/app-api/handlers/reports/create.ts +++ b/services/app-api/handlers/reports/create.ts @@ -83,11 +83,14 @@ export const createReport = handler(async (event, _context) => { let formTemplate, formTemplateVersion; + const isProgramPCCM = + unvalidatedMetadata?.programIsPCCM?.[0]?.value === "Yes"; + try { ({ formTemplate, formTemplateVersion } = await getOrCreateFormTemplate( reportBucket, reportType, - unvalidatedMetadata + isProgramPCCM )); } catch (err) { logger.error(err, "Error getting or creating template"); diff --git a/services/app-api/utils/formTemplates/formTemplates.test.ts b/services/app-api/utils/formTemplates/formTemplates.test.ts index 61d2c7e86..eb6b006af 100644 --- a/services/app-api/utils/formTemplates/formTemplates.test.ts +++ b/services/app-api/utils/formTemplates/formTemplates.test.ts @@ -2,6 +2,7 @@ import { compileValidationJsonFromRoutes, flattenReportRoutesArray, formTemplateForReportType, + generatePCCMTemplate, getOrCreateFormTemplate, getValidationFromFormTemplate, isFieldElement, @@ -15,6 +16,11 @@ import { mockDocumentClient, mockReportJson } from "../testing/setupJest"; import s3Lib from "../s3/s3-lib"; import dynamodbLib from "../dynamo/dynamodb-lib"; +const programIsPCCM = true; +const programIsNotPCCM = false; + +global.structuredClone = (val: any) => JSON.parse(JSON.stringify(val)); + const currentMLRFormHash = createHash("md5") .update(JSON.stringify(mlr)) .digest("hex"); @@ -23,11 +29,21 @@ const currentMCPARFormHash = createHash("md5") .update(JSON.stringify(mcpar)) .digest("hex"); +const pccmTemplate = generatePCCMTemplate(mcpar); +const currentPCCMFormHash = createHash("md5") + .update(JSON.stringify(pccmTemplate)) + .digest("hex"); + describe("Test getOrCreateFormTemplate MCPAR", () => { beforeEach(() => { jest.restoreAllMocks(); }); it("should create a new form template if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [], }); @@ -35,7 +51,8 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -47,7 +64,34 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { expect(result.formTemplateVersion?.md5Hash).toEqual(currentMCPARFormHash); }); + it("should create a new form template for PCCM if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); + const s3PutSpy = jest.spyOn(s3Lib, "put"); + const result = await getOrCreateFormTemplate( + "local-mcpar-reports", + ReportType.MCPAR, + programIsPCCM + ); + expect(dynamoPutSpy).toHaveBeenCalled(); + expect(s3PutSpy).toHaveBeenCalled(); + expect(result.formTemplate).toEqual({ + ...pccmTemplate, + validationJson: getValidationFromFormTemplate(pccmTemplate as ReportJson), + }); + expect(result.formTemplateVersion?.versionNumber).toEqual(1); + expect(result.formTemplateVersion?.md5Hash).toEqual(currentPCCMFormHash); + }); + it("should return the right form and formTemplateVersion if it matches the most recent form", async () => { + // mocked once for search by hash mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -56,19 +100,14 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { md5Hash: currentMCPARFormHash, versionNumber: 3, }, - { - formTemplateId: "foo", - id: "mockReportJson", - md5Hash: currentMCPARFormHash + "111", - versionNumber: 2, - }, ], }); const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).not.toHaveBeenCalled(); expect(s3PutSpy).not.toHaveBeenCalled(); @@ -77,6 +116,11 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { }); it("should create a new form if it doesn't match the most recent form", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -97,7 +141,8 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -110,6 +155,11 @@ describe("Test getOrCreateFormTemplate MLR", () => { jest.restoreAllMocks(); }); it("should create a new form template if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [], }); @@ -117,7 +167,8 @@ describe("Test getOrCreateFormTemplate MLR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -130,6 +181,7 @@ describe("Test getOrCreateFormTemplate MLR", () => { }); it("should return the right form and formTemplateVersion if it matches the most recent form", async () => { + // mocked once for search by hash mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -138,19 +190,14 @@ describe("Test getOrCreateFormTemplate MLR", () => { md5Hash: currentMLRFormHash, versionNumber: 3, }, - { - formTemplateId: "foo", - id: "mockReportJson", - md5Hash: currentMLRFormHash + "111", - versionNumber: 2, - }, ], }); const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).not.toHaveBeenCalled(); expect(s3PutSpy).not.toHaveBeenCalled(); @@ -159,18 +206,23 @@ describe("Test getOrCreateFormTemplate MLR", () => { }); it("should create a new form if it doesn't match the most recent form", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { formTemplateId: "foo", id: "mockReportJson", - md5Hash: currentMLRFormHash + "111111", + md5Hash: currentMCPARFormHash + "111111", versionNumber: 3, }, { formTemplateId: "foo", id: "mockReportJson", - md5Hash: currentMLRFormHash + "111", + md5Hash: currentMCPARFormHash + "111", versionNumber: 2, }, ], @@ -179,7 +231,8 @@ describe("Test getOrCreateFormTemplate MLR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); diff --git a/services/app-api/utils/formTemplates/formTemplates.ts b/services/app-api/utils/formTemplates/formTemplates.ts index d9751f000..ac27e6d52 100644 --- a/services/app-api/utils/formTemplates/formTemplates.ts +++ b/services/app-api/utils/formTemplates/formTemplates.ts @@ -12,7 +12,6 @@ import { FormField, FormLayoutElement, FormTemplate, - MCPARReportMetadata, ModalOverlayReportPageShape, ReportJson, ReportRoute, @@ -35,6 +34,24 @@ export async function getNewestTemplateVersion(reportType: ReportType) { return result.Items?.[0]; } +export async function getTemplateVersionByHash( + reportType: ReportType, + hash: string +) { + const queryParams: QueryInput = { + TableName: process.env.FORM_TEMPLATE_TABLE_NAME!, + IndexName: "HashIndex", + KeyConditionExpression: "reportType = :reportType AND md5Hash = :md5Hash", + Limit: 1, + ExpressionAttributeValues: { + ":md5Hash": hash as AttributeValue, + ":reportType": reportType as unknown as AttributeValue, + }, + }; + const result = await dynamodbLib.query(queryParams); + return result.Items?.[0]; +} + export const formTemplateForReportType = (reportType: ReportType) => { switch (reportType) { case ReportType.MCPAR: @@ -56,11 +73,10 @@ export const formTemplateForReportType = (reportType: ReportType) => { export async function getOrCreateFormTemplate( reportBucket: string, reportType: ReportType, - metadata: MCPARReportMetadata + isProgramPCCM: boolean ) { let currentFormTemplate = formTemplateForReportType(reportType); - // if program is PCCM generate shortened template - if (metadata?.programIsPCCM?.[0]?.value === "Yes") { + if (isProgramPCCM) { currentFormTemplate = generatePCCMTemplate(currentFormTemplate); } const stringifiedTemplate = JSON.stringify(currentFormTemplate); @@ -69,16 +85,18 @@ export async function getOrCreateFormTemplate( .update(stringifiedTemplate) .digest("hex"); - const mostRecentTemplateVersion = await getNewestTemplateVersion(reportType); - const mostRecentTemplateVersionHash = mostRecentTemplateVersion?.md5Hash; + const matchingTemplateMetadata = await getTemplateVersionByHash( + reportType, + currentTemplateHash + ); - if (currentTemplateHash === mostRecentTemplateVersionHash) { + if (matchingTemplateMetadata) { return { formTemplate: await getTemplate( reportBucket, - getFormTemplateKey(mostRecentTemplateVersion?.id) + getFormTemplateKey(matchingTemplateMetadata?.id) ), - formTemplateVersion: mostRecentTemplateVersion, + formTemplateVersion: matchingTemplateMetadata, }; } else { const newFormTemplateId = KSUID.randomSync().string; @@ -98,10 +116,12 @@ export async function getOrCreateFormTemplate( throw err; } + const newestTemplateMetadata = await getNewestTemplateVersion(reportType); + // If we didn't find any form templates, start version at 1. const newFormTemplateVersionItem: FormTemplate = { - versionNumber: mostRecentTemplateVersion?.versionNumber - ? (mostRecentTemplateVersion.versionNumber += 1) + versionNumber: newestTemplateMetadata?.versionNumber + ? (newestTemplateMetadata.versionNumber += 1) : 1, md5Hash: currentTemplateHash, id: newFormTemplateId, @@ -265,7 +285,8 @@ const routesToIncludeInPCCM = { const entitiesToIncludeInPCCM = ["plans", "sanctions"]; -export const generatePCCMTemplate = (reportTemplate: any) => { +export const generatePCCMTemplate = (originalReportTemplate: any) => { + const reportTemplate = structuredClone(originalReportTemplate); // remove top level sections not in include list reportTemplate.routes = reportTemplate.routes.filter( (route: ReportRoute) => !!routesToIncludeInPCCM[route.name]