diff --git a/src/services/ui/src/api/mocks/item.ts b/src/services/ui/src/api/mocks/item.ts index 3f9cb3575..b45cfe5ee 100644 --- a/src/services/ui/src/api/mocks/item.ts +++ b/src/services/ui/src/api/mocks/item.ts @@ -3,22 +3,41 @@ import { setupServer } from "msw/node"; import { opensearch, SEATOOL_STATUS } from "shared-types"; type GetItemBody = { id: string }; +type ItemTestFields = Pick< + opensearch.main.Document, + "id" | "seatoolStatus" | "actionType" +>; + +const ID_SEPARATOR = "-"; +type IdParamKey = keyof opensearch.main.Document; +// Because getItem(id: string) doesn't allow for easy object mocking, +// to make tests easier, you can add params to the ids your tests use +// and mock specific attributes. +// e.x. existing-approved-actionType=New +const getIdParam = (id: string, key: IdParamKey) => + id + .split(ID_SEPARATOR) + .find((param: string) => param.includes(key)) + ?.slice(key.length + 1); // + 1 to cover the `=` sign const handlers = [ http.post( "/item-mock-server", async ({ request }) => { - const { id} = await request.json(); + const { id } = await request.json(); return id.includes("existing") ? HttpResponse.json({ - _id: id, - _source: { - id: id, - seatoolStatus: id.includes("approved") ? SEATOOL_STATUS.APPROVED : SEATOOL_STATUS.PENDING - } satisfies Pick, - }) + _id: id, + _source: { + id: id, + seatoolStatus: id.includes("approved") + ? SEATOOL_STATUS.APPROVED + : SEATOOL_STATUS.PENDING, + actionType: getIdParam(id, "actionType") || "New", + } satisfies ItemTestFields, + }) : new HttpResponse(null, { status: 404 }); - } + }, ), ]; diff --git a/src/services/ui/src/api/useGetItem.test.ts b/src/services/ui/src/api/useGetItem.test.ts index 7b201f568..3d1f27150 100644 --- a/src/services/ui/src/api/useGetItem.test.ts +++ b/src/services/ui/src/api/useGetItem.test.ts @@ -44,7 +44,6 @@ describe("zod schema helpers", () => { }); afterEach(() => mockItem.server.resetHandlers()); afterAll(() => mockItem.server.close()); - describe("idIsApproved", () => { it("returns false if no getItem fails", async () => { expect(await unit.idIsApproved("not-found")).toBe(false); @@ -58,4 +57,22 @@ describe("zod schema helpers", () => { expect(await unit.idIsApproved("existing-approved")).toBe(true); }); }); + + describe("canBeRenewedOrAmended", () => { + it("returns true if item is New or Renew actionType", async () => { + const newCanRenewOrAmend = await unit.canBeRenewedOrAmended( + "existing-approved-actionType=New", + ); + const renewCanRenewOrAmend = await unit.canBeRenewedOrAmended( + "existing-approved-actionType=Renew", + ); + expect(newCanRenewOrAmend).toBe(true); + expect(renewCanRenewOrAmend).toBe(true); + }); + it("returns false if an item is Amend actionType", async () => { + expect( + await unit.canBeRenewedOrAmended("existing-approved-actionType=Amend"), + ).toBe(false); + }); + }); }); diff --git a/src/services/ui/src/api/useGetItem.ts b/src/services/ui/src/api/useGetItem.ts index 23856d0e2..792e1cac2 100644 --- a/src/services/ui/src/api/useGetItem.ts +++ b/src/services/ui/src/api/useGetItem.ts @@ -7,7 +7,7 @@ import { API } from "aws-amplify"; import { opensearch, ReactQueryApiError, SEATOOL_STATUS } from "shared-types"; export const getItem = async ( - id: string + id: string, ): Promise => await API.post("os", "/item", { body: { id } }); @@ -21,14 +21,24 @@ export const idIsApproved = async (id: string) => { } }; +export const canBeRenewedOrAmended = async (id: string) => { + try { + const record = await getItem(id); + return ["New", "Renew"].includes(record._source.actionType); + } catch (e) { + console.error(e); + return false; + } +}; + export const useGetItem = ( id: string, - options?: UseQueryOptions + options?: UseQueryOptions, ) => { return useQuery( ["record", id], () => getItem(id), - options + options, ); }; diff --git a/src/services/ui/src/utils/zod.ts b/src/services/ui/src/utils/zod.ts index d3da87fe2..7bd5690e1 100644 --- a/src/services/ui/src/utils/zod.ts +++ b/src/services/ui/src/utils/zod.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { isAuthorizedState } from "@/utils"; -import { idIsApproved, itemExists } from "@/api"; +import { canBeRenewedOrAmended, idIsApproved, itemExists } from "@/api"; export const zSpaIdSchema = z .string() @@ -98,6 +98,10 @@ export const zAmendmentOriginalWaiverNumberSchema = z message: "According to our records, this 1915(b) Waiver Number does not yet exist. Please check the 1915(b) Initial or Renewal Waiver Number and try entering it again.", }) + .refine(async (value) => canBeRenewedOrAmended(value), { + message: + "The 1915(b) Waiver Number entered does not seem to match our records. Please enter an approved 1915(b) Initial or Renewal Waiver Number, using a dash after the two character state abbreviation.", + }) .refine(async (value) => idIsApproved(value), { message: "According to our records, this 1915(b) Waiver Number is not approved. You must supply an approved 1915(b) Initial or Renewal Waiver Number.", @@ -117,6 +121,10 @@ export const zRenewalOriginalWaiverNumberSchema = z message: "According to our records, this 1915(b) Waiver Number does not yet exist. Please check the 1915(b) Initial or Renewal Waiver Number and try entering it again.", }) + .refine(async (value) => canBeRenewedOrAmended(value), { + message: + "The 1915(b) Waiver Number entered does not seem to match our records. Please enter an approved 1915(b) Initial or Renewal Waiver Number, using a dash after the two character state abbreviation.", + }) .refine(async (value) => idIsApproved(value), { message: "According to our records, this 1915(b) Waiver Number is not approved. You must supply an approved 1915(b) Initial or Renewal Waiver Number.",