diff --git a/lib/lambda/processEmailsHandler.test.ts b/lib/lambda/processEmailsHandler.test.ts index dace4fd428..0eb98d5989 100644 --- a/lib/lambda/processEmailsHandler.test.ts +++ b/lib/lambda/processEmailsHandler.test.ts @@ -7,12 +7,13 @@ import { Authority } from "shared-types"; import { SIMPLE_ID, WITHDRAW_RAI_ITEM_B, WITHDRAW_RAI_ITEM_C } from "mocks"; const nms = "new-medicaid-submission"; const ncs = "new-chip-submission"; -const tempExtension = "temp-extension"; +const tempExtension = "temporary-extension"; const withdrawPackage = "withdraw-package"; const contractingInitial = "contracting-initial"; const capitatedInitial = "capitated-initial"; const withdrawRai = "withdraw-rai"; const respondToRai = "respond-to-rai"; +const appk = "app-k"; describe("process emails Handler", () => { it.each([ @@ -131,9 +132,9 @@ describe("process emails Handler", () => { SIMPLE_ID, ], [ - `should send an email for ${withdrawPackage} with ${Authority["1915c"]}`, + `should send an email for ${appk} with ${Authority["1915c"]}`, Authority["1915c"], - withdrawPackage, + appk, SIMPLE_ID, ], [ @@ -179,9 +180,15 @@ describe("process emails Handler", () => { SIMPLE_ID, ], [ - `should send an email for ${capitatedInitial} with ${Authority["1915c"]}`, + `should send an email for ${appk} with ${Authority["1915c"]}`, Authority["1915c"], - capitatedInitial, + appk, + SIMPLE_ID, + ], + [ + `should send an email for ${appk} with ${Authority["1915b"]}`, + Authority["1915b"], + appk, SIMPLE_ID, ], [ diff --git a/lib/libs/email/content/newSubmission/emailTemplates/AppKCMS.tsx b/lib/libs/email/content/newSubmission/emailTemplates/AppKCMS.tsx index 0abdffe531..5bb1c55d7d 100644 --- a/lib/libs/email/content/newSubmission/emailTemplates/AppKCMS.tsx +++ b/lib/libs/email/content/newSubmission/emailTemplates/AppKCMS.tsx @@ -9,7 +9,7 @@ import { import { BaseEmailTemplate } from "../../email-templates"; import { formatDate } from "shared-utils"; -type AppKEmailProps = Events["NewAppKSubmission"] & CommonEmailVariables; +type AppKEmailProps = Events["AppKSubmission"] & CommonEmailVariables; // 1915c - app K export const AppKCMSEmail = ({ variables }: { variables: AppKEmailProps }) => { diff --git a/lib/libs/email/content/newSubmission/emailTemplates/AppKState.tsx b/lib/libs/email/content/newSubmission/emailTemplates/AppKState.tsx index 2d8d2b44be..441314d632 100644 --- a/lib/libs/email/content/newSubmission/emailTemplates/AppKState.tsx +++ b/lib/libs/email/content/newSubmission/emailTemplates/AppKState.tsx @@ -12,7 +12,7 @@ import { BaseEmailTemplate } from "../../email-templates"; import { styles } from "../../email-styles"; export const AppKStateEmail = (props: { - variables: Events["NewAppKSubmission"] & CommonEmailVariables; + variables: Events["AppKSubmission"] & CommonEmailVariables; }) => { const variables = props.variables; const previewText = `Appendix K Amendment Submitted`; diff --git a/lib/libs/email/content/newSubmission/index.tsx b/lib/libs/email/content/newSubmission/index.tsx index a7a967f2f3..6f16ded38c 100644 --- a/lib/libs/email/content/newSubmission/index.tsx +++ b/lib/libs/email/content/newSubmission/index.tsx @@ -61,31 +61,50 @@ export const newSubmission: AuthoritiesWithUserTypesTemplate = { variables: | (Events["CapitatedInitial"] & CommonEmailVariables & { emails: EmailAddresses }) | (Events["ContractingInitial"] & CommonEmailVariables & { emails: EmailAddresses }) - | (Events["CapitatedRenewal"] & CommonEmailVariables & { emails: EmailAddresses }), + | (Events["CapitatedRenewal"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["ContractingRenewal"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["CapitatedAmendment"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["ContractingAmendment"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["AppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }), ) => { return { to: variables.emails.osgEmail, subject: `${variables.authority} ${variables.id} Submitted`, - body: await render(), + body: await render( + variables.event === "app-k" ? ( + + ) : ( + + ), + ), }; }, state: async ( variables: | (Events["CapitatedInitial"] & CommonEmailVariables & { emails: EmailAddresses }) | (Events["ContractingInitial"] & CommonEmailVariables & { emails: EmailAddresses }) - | (Events["CapitatedRenewal"] & CommonEmailVariables & { emails: EmailAddresses }), + | (Events["CapitatedRenewal"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["ContractingRenewal"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["CapitatedAmendment"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["ContractingAmendment"] & CommonEmailVariables & { emails: EmailAddresses }) + | (Events["AppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }), ) => { return { to: [`${variables.submitterName} <${variables.submitterEmail}>`], subject: `Your ${variables.authority} ${variables.id} has been submitted to CMS`, - body: await render(), + body: await render( + variables.event === "app-k" ? ( + + ) : ( + + ), + ), }; }, }, - [Authority["1915c"]]: { cms: async ( - variables: Events["NewAppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }, + variables: Events["AppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }, ) => { return { to: variables.emails.osgEmail, @@ -94,7 +113,7 @@ export const newSubmission: AuthoritiesWithUserTypesTemplate = { }; }, state: async ( - variables: Events["NewAppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }, + variables: Events["AppKSubmission"] & CommonEmailVariables & { emails: EmailAddresses }, ) => { return { to: [`${variables.submitterName} <${variables.submitterEmail}>`], diff --git a/lib/libs/email/content/tempExtension/emailTemplates/TempExtCMS.tsx b/lib/libs/email/content/tempExtension/emailTemplates/TempExtCMS.tsx index 9a7e3a0767..72978c26f1 100644 --- a/lib/libs/email/content/tempExtension/emailTemplates/TempExtCMS.tsx +++ b/lib/libs/email/content/tempExtension/emailTemplates/TempExtCMS.tsx @@ -8,9 +8,9 @@ import { import { BaseEmailTemplate } from "../../email-templates"; import { formatNinetyDaysDate } from "shared-utils"; -export const TempExtCMSEmail = (props: { - variables: Events["TempExtension"] & CommonEmailVariables; -}) => { +type TempExtCMSEmailProps = Events["TemporaryExtension"] & CommonEmailVariables; + +export const TempExtCMSEmail = (props: { variables: TempExtCMSEmailProps }) => { const variables = props.variables; const previewText = `Temporary Extension ${variables.id} Submitted`; const heading = `The Submission Portal received a ${variables.authority} Temporary Extension Submission:`; diff --git a/lib/libs/email/content/tempExtension/emailTemplates/TempExtState.tsx b/lib/libs/email/content/tempExtension/emailTemplates/TempExtState.tsx index 8ea90ded58..96cb13c896 100644 --- a/lib/libs/email/content/tempExtension/emailTemplates/TempExtState.tsx +++ b/lib/libs/email/content/tempExtension/emailTemplates/TempExtState.tsx @@ -1,10 +1,12 @@ import { formatNinetyDaysDate } from "shared-utils"; -import { CommonEmailVariables } from "shared-types"; +import { CommonEmailVariables, Events } from "shared-types"; import { PackageDetails, MailboxNotice, FollowUpNotice, Attachments } from "../../email-components"; import { BaseEmailTemplate } from "../../email-templates"; -export const TempExtStateEmail = (props: { variables: any & CommonEmailVariables }) => { +type TempExtStateEmailProps = Events["TemporaryExtension"] & CommonEmailVariables; + +export const TempExtStateEmail = (props: { variables: TempExtStateEmailProps }) => { const variables = props.variables; const previewText = `Temporary Extension ${variables.id} Submitted`; const heading = diff --git a/lib/libs/email/content/tempExtension/index.tsx b/lib/libs/email/content/tempExtension/index.tsx index b3d4f3f011..9b34859b2f 100644 --- a/lib/libs/email/content/tempExtension/index.tsx +++ b/lib/libs/email/content/tempExtension/index.tsx @@ -6,7 +6,7 @@ import { TempExtCMSEmail, TempExtStateEmail } from "./emailTemplates"; export const tempExtention: UserTypeOnlyTemplate = { cms: async ( - variables: Events["TempExtension"] & CommonEmailVariables & { emails: EmailAddresses }, + variables: Events["TemporaryExtension"] & CommonEmailVariables & { emails: EmailAddresses }, ) => { return { to: variables.emails.osgEmail, @@ -15,7 +15,7 @@ export const tempExtention: UserTypeOnlyTemplate = { }; }, state: async ( - variables: Events["TempExtension"] & CommonEmailVariables & { emails: EmailAddresses }, + variables: Events["TemporaryExtension"] & CommonEmailVariables & { emails: EmailAddresses }, ) => { return { to: [`${variables.submitterName} <${variables.submitterEmail}>`], diff --git a/lib/libs/email/index.ts b/lib/libs/email/index.ts index ca21f9e4dd..57f4d78244 100644 --- a/lib/libs/email/index.ts +++ b/lib/libs/email/index.ts @@ -23,7 +23,7 @@ export type AuthoritiesWithUserTypesTemplate = { export type EmailTemplates = { "new-medicaid-submission": AuthoritiesWithUserTypesTemplate; "new-chip-submission": AuthoritiesWithUserTypesTemplate; - "temp-extension": UserTypeOnlyTemplate; + "temporary-extension": UserTypeOnlyTemplate; "withdraw-package": AuthoritiesWithUserTypesTemplate; "withdraw-rai": AuthoritiesWithUserTypesTemplate; "contracting-initial": AuthoritiesWithUserTypesTemplate; @@ -37,13 +37,14 @@ export type EmailTemplates = { "contracting-renewal-state": AuthoritiesWithUserTypesTemplate; "capitated-renewal-state": AuthoritiesWithUserTypesTemplate; "respond-to-rai": AuthoritiesWithUserTypesTemplate; + "app-k": AuthoritiesWithUserTypesTemplate; }; // Create a type-safe mapping of email templates const emailTemplates: EmailTemplates = { "new-medicaid-submission": EmailContent.newSubmission, "new-chip-submission": EmailContent.newSubmission, - "temp-extension": EmailContent.tempExtention, + "temporary-extension": EmailContent.tempExtention, "withdraw-package": EmailContent.withdrawPackage, "withdraw-rai": EmailContent.withdrawRai, "contracting-initial": EmailContent.newSubmission, @@ -57,6 +58,7 @@ const emailTemplates: EmailTemplates = { "contracting-renewal-state": EmailContent.newSubmission, "capitated-renewal-state": EmailContent.newSubmission, "respond-to-rai": EmailContent.respondToRai, + "app-k": EmailContent.newSubmission, }; // Create a type-safe lookup function diff --git a/lib/libs/email/mock-data/temp-extension.ts b/lib/libs/email/mock-data/temp-extension.ts index 7faf0506e5..da3a18a70a 100644 --- a/lib/libs/email/mock-data/temp-extension.ts +++ b/lib/libs/email/mock-data/temp-extension.ts @@ -3,8 +3,6 @@ export const emailTemplateValue = { territory: "MD", id: "MD-2343.R00.TE09", waiverNumber: "MD-2343.R00.TE00", - authority: "1915(b)", - actionType: "Extend", applicationEndpointUrl: "https://mako-dev.cms.gov/", get timestamp() { return Date.now() + 5184000000; diff --git a/lib/libs/email/preview/InitialSubmissions/CMS/AppK.tsx b/lib/libs/email/preview/InitialSubmissions/CMS/AppK.tsx new file mode 100644 index 0000000000..06b09119ec --- /dev/null +++ b/lib/libs/email/preview/InitialSubmissions/CMS/AppK.tsx @@ -0,0 +1,25 @@ +import { AppKCMSEmail } from "../../../content/newSubmission/emailTemplates"; +import { emailTemplateValue } from "../../../mock-data/new-submission"; +import * as attachments from "../../../mock-data/attachments"; + +const AppKCMSEmailPreview = () => { + return ( + + ); +}; + +export default AppKCMSEmailPreview; diff --git a/lib/libs/email/preview/InitialSubmissions/CMS/Temp_Extension.tsx b/lib/libs/email/preview/InitialSubmissions/CMS/Temp_Extension.tsx index 3cafcc4b2e..90cb98cdc6 100644 --- a/lib/libs/email/preview/InitialSubmissions/CMS/Temp_Extension.tsx +++ b/lib/libs/email/preview/InitialSubmissions/CMS/Temp_Extension.tsx @@ -6,6 +6,8 @@ const TempExtCMSPreview = () => { ); diff --git a/lib/libs/email/preview/InitialSubmissions/State/Temp_Extension.tsx b/lib/libs/email/preview/InitialSubmissions/State/Temp_Extension.tsx index 3a56000dcd..8713633494 100644 --- a/lib/libs/email/preview/InitialSubmissions/State/Temp_Extension.tsx +++ b/lib/libs/email/preview/InitialSubmissions/State/Temp_Extension.tsx @@ -1,5 +1,4 @@ import { emailTemplateValue } from "../../../mock-data/temp-extension"; - import { TempExtStateEmail } from "../../../content/tempExtension/emailTemplates/TempExtState"; const TempExtStatePreview = () => { @@ -7,6 +6,8 @@ const TempExtStatePreview = () => { ); diff --git a/lib/libs/email/preview/Initial_Submissions/CMS/Temp_Extension.tsx b/lib/libs/email/preview/Initial_Submissions/CMS/Temp_Extension.tsx deleted file mode 100644 index 44c222ebf1..0000000000 --- a/lib/libs/email/preview/Initial_Submissions/CMS/Temp_Extension.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { TempExtCMSEmail } from "../../../content/tempExtension/emailTemplates"; -import { emailTemplateValue } from "../../../mock-data/temp-extension"; - -const TempExtCMSPreview = () => { - return ( - - ); -}; - -export default TempExtCMSPreview; diff --git a/lib/libs/email/types.ts b/lib/libs/email/types.ts index 011dd7f514..1472916014 100644 --- a/lib/libs/email/types.ts +++ b/lib/libs/email/types.ts @@ -37,5 +37,5 @@ export interface NewSubmissionTemplateProps { } export interface TempExtensionTemplateProps { - variables: Events["TempExtension"] & CommonEmailVariables; + variables: Events["TemporaryExtension"] & CommonEmailVariables; } diff --git a/lib/packages/shared-types/events/app-k.ts b/lib/packages/shared-types/events/app-k.ts index cf258c5589..f8cd316bf5 100644 --- a/lib/packages/shared-types/events/app-k.ts +++ b/lib/packages/shared-types/events/app-k.ts @@ -33,3 +33,5 @@ export const schema = baseSchema.extend({ submitterEmail: z.string().email(), timestamp: z.number(), }); + +export type AppKSubmission = z.infer; diff --git a/lib/packages/shared-types/events/index.ts b/lib/packages/shared-types/events/index.ts index 6a61d3a879..c4bc12533a 100644 --- a/lib/packages/shared-types/events/index.ts +++ b/lib/packages/shared-types/events/index.ts @@ -45,6 +45,7 @@ export const events = { export type BaseSchemas = z.infer; export type Events = { + AppKSubmission: z.infer; CapitatedInitial: z.infer; CapitatedRenewal: z.infer; CapitatedAmendment: z.infer; @@ -53,8 +54,7 @@ export type Events = { ContractingAmendment: z.infer; NewChipSubmission: z.infer; NewMedicaidSubmission: z.infer; - TempExtension: z.infer; - NewAppKSubmission: z.infer; + TemporaryExtension: z.infer; RespondToRai: z.infer; UploadSubsequentDocuments: z.infer; WithdrawPackage: z.infer; diff --git a/mocks/data/items.ts b/mocks/data/items.ts index 78756ace8b..cd377c82f9 100644 --- a/mocks/data/items.ts +++ b/mocks/data/items.ts @@ -515,4 +515,18 @@ export const TEST_TEMP_EXT_ITEM = items[ EXISTING_ITEM_TEMPORARY_EXTENSION_ID ] as opensearch.main.ItemResult; +export const itemList = Object.values(items); + +export const getFilteredItemList = (filters: string[]) => { + return itemList.filter((item) => filters.includes(item?._source?.authority || "")); +}; + +export const docList = Object.values(items).map( + (item) => (item?._source || {}) as opensearch.main.Document, +); + +export const getFilteredDocList = (filters: string[]) => { + return docList.filter((item) => filters.includes(item?.authority || "")); +}; + export default items; diff --git a/mocks/handlers/api/search.ts b/mocks/handlers/api/search.ts index d1a51c0767..cbc80f5f72 100644 --- a/mocks/handlers/api/search.ts +++ b/mocks/handlers/api/search.ts @@ -1,12 +1,23 @@ -import { http, HttpResponse } from "msw"; -import { cpocsList } from "../../data/cpocs"; +import { http, HttpResponse, PathParams } from "msw"; +import { getFilteredItemList } from "../../data/items"; +import { getFilterValueAsStringArray } from "../search.utils"; +import { SearchQueryBody } from "../../index.d"; -const defaultApiSearchHandler = http.post( +const defaultApiSearchHandler = http.post( "https://test-domain.execute-api.us-east-1.amazonaws.com/mocked-tests/search/:index", - ({ params }) => { + async ({ params, request }) => { const { index } = params; + const { query } = await request.json(); + + const must = query?.bool?.must; + + if (index === "main") { + const authorityValues = + getFilterValueAsStringArray(must, "terms", "authority.keyword") || + getFilterValueAsStringArray(must, "terms", "authority") || + []; + const itemList = getFilteredItemList(authorityValues); - if (index === "cpocs") { return HttpResponse.json({ took: 3, timed_out: false, @@ -22,7 +33,7 @@ const defaultApiSearchHandler = http.post( relation: "eq", }, max_score: 1, - hits: cpocsList, + hits: itemList, }, }); } diff --git a/mocks/handlers/search.utils.ts b/mocks/handlers/search.utils.ts index 608a7b2e13..9290c7ea03 100644 --- a/mocks/handlers/search.utils.ts +++ b/mocks/handlers/search.utils.ts @@ -109,6 +109,38 @@ const parseValueAsNumberArray = (value: string | string[] | undefined): number[] ); }; +export const getFilterValueAsString = ( + query: QueryContainer | QueryContainer[] | undefined, + queryKey: keyof QueryContainer, + filterName: string, +): string | undefined => { + const value = getFilterValue(query, queryKey, filterName); + + return parseValueAsStringArray(value).join(","); +}; + +export const getFilterValueAsStringArray = ( + query: QueryContainer | QueryContainer[] | undefined, + queryKey: keyof QueryContainer, + filterName: string, +): string[] => { + const value = getFilterValue(query, queryKey, filterName); + + return parseValueAsStringArray(value); +}; + +const parseValueAsStringArray = (value: string | string[] | undefined): string[] => { + if (value == undefined) { + return []; + } + + if (typeof value === "string") { + return value.split(",").map((val) => val.trim()); + } + + return value.filter((val) => val && typeof val === "string").map((val) => val.trim()) || []; +}; + export const getTermValues = ( query: QueryContainer | QueryContainer[] | undefined, filterName: string, diff --git a/react-app/src/api/useSearch.test.ts b/react-app/src/api/useSearch.test.ts index c412d53ea7..fa4a2d4216 100644 --- a/react-app/src/api/useSearch.test.ts +++ b/react-app/src/api/useSearch.test.ts @@ -1,21 +1,21 @@ import { describe, expect, it } from "vitest"; -import { getOsData } from "./useSearch"; -import { cpocsList } from "mocks/data/cpocs"; +import { getMainExportData } from "./useSearch"; +import { DEFAULT_FILTERS } from "@/components/Opensearch/main/useOpensearch"; +import { getFilteredDocList } from "mocks/data/items"; -describe("getOsData tests", () => { - it("should return cpocs", async () => { - const results = await getOsData({ - index: "cpocs", - sort: { - field: "lastName", - order: "asc", - }, - pagination: { - number: 0, - size: 20, - }, - filters: [], - }); - expect(results.hits.hits).toEqual(cpocsList); +describe("getMainExportData tests", () => { + it("should return spa items", async () => { + const results = await getMainExportData(DEFAULT_FILTERS.spas.filters); + expect(results).toEqual(getFilteredDocList(["Medicaid SPA", "CHIP SPA"])); + }); + + it("should return waiver items", async () => { + const results = await getMainExportData(DEFAULT_FILTERS.waivers.filters); + expect(results).toEqual(getFilteredDocList(["1915(b)", "1915(c)"])); + }); + + it("should return an empty array if there are no filters", async () => { + const results = await getMainExportData(); + expect(results).toEqual([]); }); }); diff --git a/react-app/src/components/Opensearch/utils.test.ts b/react-app/src/components/Opensearch/utils.test.ts new file mode 100644 index 0000000000..f6b701fa68 --- /dev/null +++ b/react-app/src/components/Opensearch/utils.test.ts @@ -0,0 +1,688 @@ +import { describe, expect, it } from "vitest"; +import { + filterQueryBuilder, + paginationQueryBuilder, + sortQueryBuilder, + aggQueryBuilder, + createSearchFilterable, + checkMultiFilter, +} from "./utils"; + +describe("Opensearch utils tests", () => { + describe("filterQueryBuilder tests", () => { + it("should handle undefined filters", () => { + const results = filterQueryBuilder(undefined); + expect(results).toEqual({}); + }); + it("should handle null filters", () => { + const results = filterQueryBuilder(null); + expect(results).toEqual({}); + }); + it("should handle empty filters", () => { + const results = filterQueryBuilder([]); + expect(results).toEqual({}); + }); + it("should handle exists filter without a value", () => { + const results = filterQueryBuilder([ + // @ts-expect-error + { + type: "exists", + prefix: "must", + field: "origin", + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: [{ exists: { field: "origin" } }], + must_not: [], + should: [], + filter: [], + }, + }, + }); + }); + it("should handle an invalid prefix", () => { + const result = filterQueryBuilder([ + { + // @ts-expect-error + type: "mismatch", + prefix: "must", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA"], + }, + ]); + expect(result).toEqual({ + query: { + bool: { + must: [], + must_not: [], + should: [], + filter: [], + }, + }, + }); + }); + it.each([["terms"], ["term"], ["range"], ["global_search"]])( + "should handle %s filter without a value", + (type) => { + const results = filterQueryBuilder([ + { + // @ts-expect-error + type, + prefix: "must", + field: "authority.keyword", + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: [], + must_not: [], + should: [], + filter: [], + }, + }, + }); + }, + ); + it("should handle match filter with a false value", () => { + const results = filterQueryBuilder([ + { + type: "match", + prefix: "must", + field: "authority.keyword", + value: false, + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: [{ match: { "authority.keyword": false } }], + must_not: [], + should: [], + filter: [], + }, + }, + }); + }); + it.each([ + [ + "terms", + "authority.keyword", + ["Medicaid SPA", "CHIP SPA"], + [{ terms: { "authority.keyword": ["Medicaid SPA", "CHIP SPA"] } }], + ], + ["term", "state", "MD", [{ term: { state: "MD" } }]], + ["exists", "origin", "OneMAC", [{ exists: { field: "origin" } }]], + [ + "range", + "timestamp", + ["1677715190000", "1677715210000"], + [{ range: { timestamp: ["1677715190000", "1677715210000"] } }], + ], + ])("should handle must %s filters", (type, field, value, expected) => { + const results = filterQueryBuilder([ + { + // @ts-expect-error + type, + prefix: "must", + field, + value, + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: expected, + must_not: [], + should: [], + filter: [], + }, + }, + }); + }); + it("should handle a global_search filter", () => { + const results = filterQueryBuilder([ + { + type: "global_search", + prefix: "must", + field: "authority", + value: " CHIP SPA ", + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: [ + { + dis_max: { + tie_breaker: 0.7, + boost: 1.2, + queries: [ + { + wildcard: { + "id.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + { + wildcard: { + "submitterName.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + { + wildcard: { + "leadAnalystName.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + ], + }, + }, + ], + must_not: [], + should: [], + filter: [], + }, + }, + }); + }); + it("should handle multiple filters", () => { + const results = filterQueryBuilder([ + { + type: "terms", + prefix: "must", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA"], + }, + { + type: "term", + prefix: "must_not", + field: "state", + value: "MD", + }, + // @ts-expect-error + { + type: "exists", + prefix: "should", + field: "origin", + }, + { + type: "range", + prefix: "filter", + field: "timestamp", + value: ["1677715190000", "1677715210000"], + }, + { + type: "global_search", + prefix: "must", + field: "authority", + value: " CHIP SPA ", + }, + ]); + expect(results).toEqual({ + query: { + bool: { + must: [ + { terms: { "authority.keyword": ["Medicaid SPA", "CHIP SPA"] } }, + { + dis_max: { + tie_breaker: 0.7, + boost: 1.2, + queries: [ + { + wildcard: { + "id.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + { + wildcard: { + "submitterName.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + { + wildcard: { + "leadAnalystName.keyword": { + value: "*CHIP SPA*", + case_insensitive: true, + }, + }, + }, + ], + }, + }, + ], + must_not: [{ term: { state: "MD" } }], + should: [{ exists: { field: "origin" } }], + filter: [{ range: { timestamp: ["1677715190000", "1677715210000"] } }], + }, + }, + }); + }); + }); + + describe("paginationQueryBuilder tests", () => { + it("should handle an undefined pagination", () => { + const result = paginationQueryBuilder(undefined); + expect(result).toEqual({}); + }); + it("should handle a null pagination", () => { + const result = paginationQueryBuilder(null); + expect(result).toEqual({}); + }); + it("should handle an empty pagination", () => { + // @ts-expect-error + const result = paginationQueryBuilder({}); + expect(result).toEqual({ + from: 0, + size: 25, + }); + }); + it("should handle an undefined number", () => { + // @ts-expect-error + const result = paginationQueryBuilder({ size: 20 }); + expect(result).toEqual({ + from: 0, + size: 20, + }); + }); + it("should handle 0 number", () => { + const result = paginationQueryBuilder({ number: 0, size: 20 }); + expect(result).toEqual({ + from: 0, + size: 20, + }); + }); + it("should handle a negative number", () => { + const result = paginationQueryBuilder({ number: -3, size: 20 }); + expect(result).toEqual({ + from: 0, + size: 20, + }); + }); + it("should handle an undefined size", () => { + // @ts-expect-error + const result = paginationQueryBuilder({ number: 1 }); + expect(result).toEqual({ + from: 0, + size: 25, + }); + }); + it("should handle 0 size", () => { + const result = paginationQueryBuilder({ number: 1, size: 0 }); + expect(result).toEqual({ + from: 0, + size: 25, + }); + }); + it("should handle a negative size", () => { + const result = paginationQueryBuilder({ number: 1, size: -3 }); + expect(result).toEqual({ + from: 0, + size: 25, + }); + }); + it("should handle a valid number and size", () => { + const result = paginationQueryBuilder({ number: 2, size: 20 }); + expect(result).toEqual({ + from: 40, + size: 20, + }); + }); + }); + + describe("sortQueryBuilder tests", () => { + it("should handle an undefined sort", () => { + const result = sortQueryBuilder(undefined); + expect(result).toEqual({}); + }); + it("should handle a null sort", () => { + const result = sortQueryBuilder(null); + expect(result).toEqual({}); + }); + it("should handle an empty sort", () => { + // @ts-expect-error + const result = sortQueryBuilder({}); + expect(result).toEqual({}); + }); + it("should handle an undefined field", () => { + // @ts-expect-error + const result = sortQueryBuilder({ order: "asc" }); + expect(result).toEqual({}); + }); + it("should handle a null field", () => { + const result = sortQueryBuilder({ field: null, order: "asc" }); + expect(result).toEqual({}); + }); + it("should handle an undefined order", () => { + // @ts-expect-error + const result = sortQueryBuilder({ field: "test" }); + expect(result).toEqual({ sort: [{ test: "asc" }] }); + }); + it("should handle a null field", () => { + const result = sortQueryBuilder({ field: "test", order: null }); + expect(result).toEqual({ sort: [{ test: "asc" }] }); + }); + it("should handle a valid sort", () => { + const result = sortQueryBuilder({ field: "test", order: "desc" }); + expect(result).toEqual({ sort: [{ test: "desc" }] }); + }); + }); + + describe("aggQueryBuilder tests", () => { + it("should handle an undefined aggregation", () => { + const result = aggQueryBuilder(undefined); + expect(result).toEqual({}); + }); + it("should handle a null aggregation", () => { + const result = aggQueryBuilder(null); + expect(result).toEqual({}); + }); + it("should handle an empty aggregation", () => { + const result = aggQueryBuilder([]); + expect(result).toEqual({}); + }); + it("should handle an aggregation with an undefined name", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + type: "term", + field: "authority.keyword", + size: 25, + }, + ]); + expect(result).toEqual({ + aggs: {}, + }); + }); + it("should handle an aggregation with an undefined type", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + field: "authority.keyword", + size: 25, + }, + ]); + expect(result).toEqual({ + aggs: {}, + }); + }); + it("should handle an aggregation with an undefined field", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + type: "term", + size: 25, + }, + ]); + expect(result).toEqual({ + aggs: {}, + }); + }); + it("should handle an aggregation with an undefined size", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + type: "term", + field: "authority.keyword", + }, + ]); + expect(result).toEqual({ + aggs: { + must: { + term: { + field: "authority.keyword", + order: { _term: "asc" }, + }, + }, + }, + }); + }); + it("should handle an aggregation with one definition", () => { + const result = aggQueryBuilder([ + { + name: "must", + type: "term", + field: "authority.keyword", + size: 50, + }, + ]); + expect(result).toEqual({ + aggs: { + must: { + term: { + field: "authority.keyword", + order: { _term: "asc" }, + size: 50, + }, + }, + }, + }); + }); + it("should handle an aggregation with multiple definitions", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + type: "terms", + field: "state", + }, + { + name: "must_not", + type: "match", + field: "origin", + size: 40, + }, + ]); + expect(result).toEqual({ + aggs: { + must: { + terms: { + field: "state", + order: { _term: "asc" }, + }, + }, + must_not: { + match: { + field: "origin", + order: { _term: "asc" }, + size: 40, + }, + }, + }, + }); + }); + it("should overwrite aggregations with the same name and type", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + type: "terms", + field: "state", + }, + { + name: "must", + type: "terms", + field: "origin", + size: 40, + }, + ]); + expect(result).toEqual({ + aggs: { + must: { + terms: { + field: "origin", + order: { _term: "asc" }, + size: 40, + }, + }, + }, + }); + }); + it("should over aggregations with the same name and different types", () => { + const result = aggQueryBuilder([ + // @ts-expect-error + { + name: "must", + type: "terms", + field: "state", + }, + { + name: "must", + type: "match", + field: "origin", + size: 40, + }, + ]); + expect(result).toEqual({ + aggs: { + must: { + match: { + field: "origin", + order: { _term: "asc" }, + size: 40, + }, + }, + }, + }); + }); + }); + + describe("createSearchFilterable tests", () => { + it("should handle an undefined value", () => { + const result = createSearchFilterable(undefined); + expect(result).toEqual([]); + }); + it("should handle a null value", () => { + const result = createSearchFilterable(null); + expect(result).toEqual([]); + }); + it("should handle an empty value", () => { + const result = createSearchFilterable(""); + expect(result).toEqual([]); + }); + it("should handle a value", () => { + const result = createSearchFilterable("test"); + expect(result).toEqual([ + { + type: "global_search", + field: "", + value: "test", + prefix: "must", + }, + ]); + }); + }); + + describe("checkMultiFilter tests", () => { + it("should return true undefined filters and undefined val", () => { + const result = checkMultiFilter(undefined, undefined); + expect(result).toBe(true); + }); + it("should return true for equal filters and val", () => { + const result = checkMultiFilter( + [ + { + prefix: "must", + type: "terms", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA"], + }, + ], + 1, + ); + expect(result).toBe(true); + }); + it("should return true for more filters than val", () => { + const result = checkMultiFilter( + [ + { + prefix: "must", + type: "terms", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA"], + }, + { + prefix: "must", + type: "exists", + field: "origin", + value: true, + }, + ], + 1, + ); + expect(result).toBe(true); + }); + it("should return true for more filter values than val", () => { + const result = checkMultiFilter( + [ + { + prefix: "must", + type: "terms", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA", "1915(b)", "1915(c)"], + }, + { + prefix: "must", + type: "exists", + field: "origin", + value: true, + }, + ], + 3, + ); + expect(result).toBe(true); + }); + it("should return false for less filters than val", () => { + const result = checkMultiFilter( + [ + { + prefix: "must", + type: "terms", + field: "authority.keyword", + value: ["Medicaid SPA", "CHIP SPA"], + }, + { + prefix: "must", + type: "exists", + field: "origin", + value: true, + }, + ], + 4, + ); + expect(result).toBe(false); + }); + it("should return false for filter values less than val", () => { + const result = checkMultiFilter( + [ + { + prefix: "must", + type: "terms", + field: "authority.keyword", + value: ["Medicaid SPA"], + }, + ], + 2, + ); + expect(result).toBe(false); + }); + }); +}); diff --git a/react-app/src/components/Opensearch/utils.ts b/react-app/src/components/Opensearch/utils.ts index bc73ea90c3..d0e20443e6 100644 --- a/react-app/src/components/Opensearch/utils.ts +++ b/react-app/src/components/Opensearch/utils.ts @@ -4,6 +4,14 @@ const filterMapQueryReducer = ( state: Record["prefix"], any[]>, filter: opensearch.Filterable, ) => { + if (filter.type === "exists") { + state[filter.prefix].push({ + exists: { field: filter.field }, + }); + } + + if (filter.value === undefined || filter.value == null) return state; + // this was hoisted up since false is a valid "match" value if (filter.type === "match") { state[filter.prefix].push({ @@ -25,12 +33,6 @@ const filterMapQueryReducer = ( }); } - if (filter.type === "exists") { - state[filter.prefix].push({ - exists: { field: filter.field }, - }); - } - if (filter.type === "range") { state[filter.prefix].push({ range: { [filter.field]: filter.value }, @@ -38,7 +40,6 @@ const filterMapQueryReducer = ( } if (filter.type === "global_search") { - if (!filter.value) return state; state[filter.prefix].push({ dis_max: { tie_breaker: 0.7, @@ -74,31 +75,45 @@ export const filterQueryBuilder = (filters: opensearch.Filterable[]) => { }; export const paginationQueryBuilder = (pagination: opensearch.QueryState["pagination"]) => { + if (!pagination) return {}; + const from = (() => { - if (!pagination.number) return 0; + if ( + !pagination?.number || + !pagination?.size || + pagination?.number < 1 || + pagination?.size < 1 + ) { + return 0; + } return pagination.number * pagination.size; })(); return { - size: pagination.size, + size: pagination?.size && pagination?.size > 0 ? pagination?.size : 25, from, }; }; export const sortQueryBuilder = (sort: opensearch.QueryState["sort"]) => { - return { sort: [{ [sort.field]: sort.order }] }; + if (!sort?.field) return {}; + return { sort: [{ [sort.field]: sort.order || "asc" }] }; }; export const aggQueryBuilder = (aggs: opensearch.AggQuery[]) => { + if (!aggs?.length) return {}; + return { aggs: aggs.reduce((STATE, AGG) => { - STATE[AGG.name] = { - [AGG.type]: { - field: AGG.field, - order: { _term: "asc" }, - ...(AGG.size && { size: AGG.size }), - }, - }; + if (AGG?.name && AGG?.type && AGG?.field) { + STATE[AGG.name] = { + [AGG.type]: { + field: AGG.field, + order: { _term: "asc" }, + ...(AGG.size && { size: AGG.size }), + }, + }; + } return STATE; }, {} as any), }; @@ -116,7 +131,7 @@ export const createSearchFilterable = (value?: string) => { ]; }; -export const checkMultiFilter = (filters: opensearch.Filterable[], val: number) => { +export const checkMultiFilter = (filters: opensearch.Filterable[] = [], val: number = 0) => { return ( filters.length >= val || filters.some((filter) => Array.isArray(filter.value) && filter.value.length >= val) diff --git a/react-app/src/features/forms/post-submission/toggle-withdraw-rai/__snapshots__/index.test.tsx.snap b/react-app/src/features/forms/post-submission/toggle-withdraw-rai/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..914c34bc0b --- /dev/null +++ b/react-app/src/features/forms/post-submission/toggle-withdraw-rai/__snapshots__/index.test.tsx.snap @@ -0,0 +1,851 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Toggle Withdraw Rai components > renders disable withdraw rai correctly 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+ +
+
+

+ Enable Formal RAI Response Withdraw Details +

+
+
+
+
+ Once you submit this form, the most recent Formal RAI Response for this package will be able to be withdrawn by the state +
+
+
+
+

+ Waiver Number +

+

+ VA-2234.R11.02 +

+
+
+

+ Authority +

+

+ 1915(b) Waiver +

+
+
+
+
+
+ + +
+
+ +
+ , +
+ , + "container":
+
+ +
+
+

+ Enable Formal RAI Response Withdraw Details +

+
+
+
+
+ Once you submit this form, the most recent Formal RAI Response for this package will be able to be withdrawn by the state +
+
+
+
+

+ Waiver Number +

+

+ VA-2234.R11.02 +

+
+
+

+ Authority +

+

+ 1915(b) Waiver +

+
+
+
+
+
+ + +
+
+ +
+ , +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`Toggle Withdraw Rai components > renders disable withdraw rai correctly 2`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+ +
+
+

+ Disable Formal RAI Response Withdraw Details +

+
+
+
+
+ The state will not be able to withdraw its RAI response. It may take up to a minute for this change to be applied. +
+
+
+
+

+ Waiver Number +

+

+ VA-2234.R11.02 +

+
+
+

+ Authority +

+

+ 1915(b) Waiver +

+
+
+
+
+
+ + +
+
+ +
+ , +
+ , + "container":
+
+ +
+
+

+ Disable Formal RAI Response Withdraw Details +

+
+
+
+
+ The state will not be able to withdraw its RAI response. It may take up to a minute for this change to be applied. +
+
+
+
+

+ Waiver Number +

+

+ VA-2234.R11.02 +

+
+
+

+ Authority +

+

+ 1915(b) Waiver +

+
+
+
+
+
+ + +
+
+ +
+ , +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/react-app/src/features/forms/post-submission/toggle-withdraw-rai/index.test.tsx b/react-app/src/features/forms/post-submission/toggle-withdraw-rai/index.test.tsx new file mode 100644 index 0000000000..552afa0845 --- /dev/null +++ b/react-app/src/features/forms/post-submission/toggle-withdraw-rai/index.test.tsx @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest"; + +import { WITHDRAW_RAI_ITEM_B } from "mocks"; +import { EnableWithdrawRaiForm, DisableWithdrawRaiForm } from "."; +import { renderFormAsync } from "@/utils/test-helpers/renderForm"; + +vi.mock("react-router", async () => ({ + ...(await vi.importActual>("react-router")), + useParams: vi.fn().mockReturnValue({ authority: "1915(b)", id: WITHDRAW_RAI_ITEM_B }), +})); +vi.mock("shared-utils", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(typeof actual === "object" && actual !== null ? actual : {}), + isCmsUser: vi.fn().mockReturnValue(true), + }; +}); + +describe("Toggle Withdraw Rai components", () => { + it("renders disable withdraw rai correctly", async () => { + const container = renderFormAsync(); + + expect(await container).toMatchSnapshot(); + }); + it("renders disable withdraw rai correctly", async () => { + const container = renderFormAsync(); + + expect(await container).toMatchSnapshot(); + }); +}); diff --git a/react-app/src/features/forms/post-submission/withdraw-package/__snapshots__/indext.test.tsx.snap b/react-app/src/features/forms/post-submission/withdraw-package/__snapshots__/indext.test.tsx.snap new file mode 100644 index 0000000000..2fc5fae390 --- /dev/null +++ b/react-app/src/features/forms/post-submission/withdraw-package/__snapshots__/indext.test.tsx.snap @@ -0,0 +1,876 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`WithdrawPackageAction components > renders WithdrawPackageActionWaiver correctly 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+ +
+
+

+ Withdraw 1915(c) Appendix K +

+
+
+
+ + * + + + + Indicates a required field. + +
+ Complete this form to withdraw this 1915(c) Appendix K package. Once complete, you will not be able to resubmit this package. CMS will be notified and will use this content to review your request. If CMS needs any additional information, they will follow up by email. +

+ If you leave this page, you will lose your progress on this form. +

+
+
+
+
+

+ Waiver Number +

+

+ VA-2234.R11.01 +

+
+
+

+ Authority +

+

+ 1915(c) Waiver +

+
+
+
+
+
+

+ Attachments + +

+
+
+
+

+ Upload your supporting documentation for withdrawal or explain your need for withdrawal in the Additional Information section. +

+
+

+ Maximum file size of 80 MB per attachment. + + + You can add multiple files per attachment type. + + Read the description for each of the attachment types on the + + + FAQ Page + + . +

+
+

+ We accept the following file formats: + + + .doc, .docx, .pdf, .jpg, .xlsx, and more. + + + + See the full list + + . +

+
+
+
+ + +
+
+
+
+
+

+ Additional Information + +

+
+
+
+ +