From 2661a999230dc8e02a3202cfe1a5d86ecc96cd08 Mon Sep 17 00:00:00 2001 From: Mike Dial <48921055+mdial89f@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:31:24 -0500 Subject: [PATCH] fix(UI bugs): Various UI bug fixes, none major (#403) * add case insensitive isunique search * add itemExists * Revert "add case insensitive isunique search" This reverts commit a4a4c80df4ecbf0f797155dae6c3d3223d740d5e. * case insensitive item exists func * remove console logs * clear enabled status when a package is withdraw etc * fix filter drawer to use the next business day * Add missing language * Update titles * remove old pattern * Fix(OS): Update OS item exists workflow * fix issue where records were throwing validation errors needlessly * fixes * Revert "Fix(OS): Update OS item exists workflow" This reverts commit b3ef1e07926c6c1643c72f2649cf024a4c72a2e6. --------- Co-authored-by: Paul Kim --- src/packages/shared-types/attachments.ts | 8 +-- .../opensearch/main/transforms/seatool.ts | 13 ++-- .../shared-utils/seatool-date-helper.ts | 60 +++++++++++-------- src/services/api/handlers/itemExists.ts | 41 +++++++++++++ src/services/api/serverless.yml | 18 ++++++ src/services/data/handlers/sinkMain.ts | 7 ++- src/services/ui/src/api/index.ts | 1 + src/services/ui/src/api/itemExists.ts | 6 ++ src/services/ui/src/api/useGetItem.ts | 9 --- .../Filtering/Drawer/Filterable/DateRange.tsx | 3 +- src/services/ui/src/pages/form/zod.ts | 15 +++-- 11 files changed, 126 insertions(+), 55 deletions(-) create mode 100644 src/services/api/handlers/itemExists.ts create mode 100644 src/services/ui/src/api/itemExists.ts diff --git a/src/packages/shared-types/attachments.ts b/src/packages/shared-types/attachments.ts index 2948a1962..f06812ed9 100644 --- a/src/packages/shared-types/attachments.ts +++ b/src/packages/shared-types/attachments.ts @@ -7,12 +7,12 @@ export const attachmentTitleMap: Record = { currentStatePlan: "Current State Plan", spaPages: "SPA Pages", coverLetter: "Cover Letter", - tribalEngagement: "Tribal Engagement", - existingStatePlanPages: "Existing State Plan Pages", + tribalEngagement: "Document Demonstrating Good-Faith Tribal Engagement", + existingStatePlanPages: "Existing State Plan Page(s)", publicNotice: "Public Notice", - sfq: "SFQ", + sfq: "Standard Funding Questions (SFQs)", tribalConsultation: "Tribal Consultation", - amendedLanguage: "Amended Language", + amendedLanguage: "Amended State Plan Language", budgetDocuments: "Budget Documents", // ISSUE RAI formalRaiLetter: "Formal RAI Letter", diff --git a/src/packages/shared-types/opensearch/main/transforms/seatool.ts b/src/packages/shared-types/opensearch/main/transforms/seatool.ts index 43ebcaf3a..bab175bfb 100644 --- a/src/packages/shared-types/opensearch/main/transforms/seatool.ts +++ b/src/packages/shared-types/opensearch/main/transforms/seatool.ts @@ -7,10 +7,7 @@ import { SeatoolOfficer, } from "../../.."; -import { - Authority, - SEATOOL_AUTHORITIES, -} from "shared-types"; +import { Authority, SEATOOL_AUTHORITIES } from "shared-types"; type Flavor = "SPA" | "WAIVER" | "MEDICAID" | "CHIP"; @@ -150,7 +147,7 @@ export const transform = (id: string) => { const authorityId = data.PLAN_TYPES?.[0].PLAN_TYPE_ID; const typeId = data.STATE_PLAN_SERVICETYPES?.[0]?.SERVICE_TYPE_ID; const subTypeId = data.STATE_PLAN_SERVICE_SUBTYPES?.[0]?.SERVICE_SUBTYPE_ID; - return { + const resp = { id, flavor: flavorLookup(data.STATE_PLAN.PLAN_TYPE), // This is MEDICAID CHIP or WAIVER... our concept actionType: data.ACTIONTYPES?.[0].ACTION_NAME, @@ -189,7 +186,11 @@ export const transform = (id: string) => { seatoolStatus, flavorLookup(data.STATE_PLAN.PLAN_TYPE) ), + raiWithdrawEnabled: !finalDispositionStatuses.includes(cmsStatus) + ? undefined + : true, }; + return resp; }); }; export type Schema = ReturnType; @@ -220,4 +221,4 @@ export const tombstone = (id: string) => { submissionDate: null, subject: null, }; -}; \ No newline at end of file +}; diff --git a/src/packages/shared-utils/seatool-date-helper.ts b/src/packages/shared-utils/seatool-date-helper.ts index b6a7e8f02..3da606ec1 100644 --- a/src/packages/shared-utils/seatool-date-helper.ts +++ b/src/packages/shared-utils/seatool-date-helper.ts @@ -1,47 +1,55 @@ import moment from "moment-timezone"; -import * as fedHolidays from '@18f/us-federal-holidays'; +import * as fedHolidays from "@18f/us-federal-holidays"; +// Takes a local epoch for a moment in time, and returns the UTC epoch for that same moment +export const offsetToUtc = (date: Date): Date => { + return new Date(date.getTime() - date.getTimezoneOffset() * 60000); +}; -// This manually accounts for the offset between the client's timezone and UTC. -export const offsetForUtc = (date: Date): Date => { - return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); -} +// Takes a UTC epoch for a moment in time, and returns the local epoch for that same moment +export const offsetFromUtc = (date: Date): Date => { + return new Date(date.getTime() + date.getTimezoneOffset() * 60000); +}; // This creates a Date for midnight today, then accounts for timezone offset. export const seaToolFriendlyTimestamp = (date?: Date): number => { // If you don't pass a date, we assume you want today the timestamp for today, midnight, utc. - if(!date) { + if (!date) { date = new Date(); - date.setHours(0,0,0,0); + date.setHours(0, 0, 0, 0); } - return offsetForUtc(date).getTime(); + return offsetToUtc(date).getTime(); }; // This takes an epoch string and converts it to a standard format for display export const formatSeatoolDate = (date: string): string => { - return moment(date).tz("UTC").format("MM/DD/yyyy") -} + return moment(date).tz("UTC").format("MM/DD/yyyy"); +}; -export const getNextBusinessDayTimestamp = (date: Date = new Date()): number => { - let localeStringDate = date.toLocaleString("en-US", { timeZone: "America/New_York", dateStyle: "short" }); - let localeStringHours24 = date.toLocaleString("en-US", { timeZone: "America/New_York", hour: 'numeric', hour12: false }); +export const getNextBusinessDayTimestamp = ( + date: Date = new Date() +): number => { + let localeStringDate = date.toLocaleString("en-US", { + timeZone: "America/New_York", + dateStyle: "short", + }); + let localeStringHours24 = date.toLocaleString("en-US", { + timeZone: "America/New_York", + hour: "numeric", + hour12: false, + }); let localeDate = new Date(localeStringDate); - - console.log(`Evaluating ${localeStringDate} at ${localeStringHours24}`); - - const after5pmEST = parseInt(localeStringHours24,10) >= 17 - const isHoliday = fedHolidays.isAHoliday(localeDate) - const isWeekend = !(localeDate.getDay() % 6) - if(after5pmEST || isHoliday || isWeekend) { + const after5pmEST = parseInt(localeStringHours24, 10) >= 17; + const isHoliday = fedHolidays.isAHoliday(localeDate); + const isWeekend = !(localeDate.getDay() % 6); + if (after5pmEST || isHoliday || isWeekend) { let nextDate = localeDate; nextDate.setDate(nextDate.getDate() + 1); - nextDate.setHours(12,0,0,0) - console.log("Current date is not valid. Will try " + nextDate) - return getNextBusinessDayTimestamp(nextDate) + nextDate.setHours(12, 0, 0, 0); + return getNextBusinessDayTimestamp(nextDate); } // Return the next business day's epoch for midnight UTC - let ret = offsetForUtc(localeDate).getTime(); - console.log('Current date is a valid business date. Will return ' + ret); + let ret = offsetToUtc(localeDate).getTime(); return ret; -} \ No newline at end of file +}; diff --git a/src/services/api/handlers/itemExists.ts b/src/services/api/handlers/itemExists.ts new file mode 100644 index 000000000..9ee7cbf20 --- /dev/null +++ b/src/services/api/handlers/itemExists.ts @@ -0,0 +1,41 @@ +import { response } from "../libs/handler"; +import { APIGatewayEvent } from "aws-lambda"; +import * as os from "../../../libs/opensearch-lib"; + +export const handler = async (event: APIGatewayEvent) => { + if (!event.body) { + return response({ + statusCode: 400, + body: { message: "Event body required" }, + }); + } + try { + const body = JSON.parse(event.body); + const packageResult = await os.search(process.env.osDomain!, "main", { + query: { + match_phrase: { + id: { + query: body.id, + }, + }, + }, + }); + if (packageResult?.hits.total.value == 0) { + return response({ + statusCode: 200, + body: { message: "No record found for the given id", exists: false }, + }); + } else { + return response({ + statusCode: 200, + body: { message: "Record found for the given id", exists: true }, + }); + } + } catch (error) { + console.error({ error }); + return response({ + statusCode: 500, + body: { message: "Internal server error" }, + }); + } +}; diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index aac5ce62a..80a637c42 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -184,6 +184,24 @@ functions: subnetIds: >- ${self:custom.vpc.privateSubnets} provisionedConcurrency: ${param:itemProvisionedConcurrency} + itemExists: + handler: handlers/itemExists.handler + maximumRetryAttempts: 0 + environment: + region: ${self:provider.region} + osDomain: ${param:osDomain} + events: + - http: + path: /itemExists + method: post + cors: true + authorizer: aws_iam + vpc: + securityGroupIds: + - Ref: SecurityGroup + subnetIds: >- + ${self:custom.vpc.privateSubnets} + provisionedConcurrency: ${param:itemProvisionedConcurrency} submit: handler: handlers/submit.handler maximumRetryAttempts: 0 diff --git a/src/services/data/handlers/sinkMain.ts b/src/services/data/handlers/sinkMain.ts index 0e989bcf2..78128fe67 100644 --- a/src/services/data/handlers/sinkMain.ts +++ b/src/services/data/handlers/sinkMain.ts @@ -171,7 +171,12 @@ const onemac = async (kafkaRecords: KafkaRecord[], topicPartition: string) => { .safeParse(record); } })(); - + if (result === undefined) { + console.log( + `no action to take for ${id} action ${record.actionType}. Continuing...` + ); + continue; + } if (!result?.success) { logError({ type: ErrorType.VALIDATION, diff --git a/src/services/ui/src/api/index.ts b/src/services/ui/src/api/index.ts index 9db038348..16ed85090 100644 --- a/src/services/ui/src/api/index.ts +++ b/src/services/ui/src/api/index.ts @@ -3,3 +3,4 @@ export * from "./useGetForm"; export * from "./useGetItem"; export * from "./getAttachmentUrl"; export * from "./useGetPackageActions"; +export * from "./itemExists"; diff --git a/src/services/ui/src/api/itemExists.ts b/src/services/ui/src/api/itemExists.ts new file mode 100644 index 000000000..b3ebb7202 --- /dev/null +++ b/src/services/ui/src/api/itemExists.ts @@ -0,0 +1,6 @@ +import { API } from "aws-amplify"; + +export const itemExists = async (id: string): Promise => { + const response = await API.post("os", "/itemExists", { body: { id } }); + return response.exists; +}; diff --git a/src/services/ui/src/api/useGetItem.ts b/src/services/ui/src/api/useGetItem.ts index 63ab8426c..7b43bc64b 100644 --- a/src/services/ui/src/api/useGetItem.ts +++ b/src/services/ui/src/api/useGetItem.ts @@ -7,15 +7,6 @@ export const getItem = async ( ): Promise => await API.post("os", "/item", { body: { id } }); -export const idIsUnique = async (id: string) => { - try { - await getItem(id); - return false; - } catch (e) { - return true; - } -}; - export const idIsApproved = async (id: string) => { try { const record = await getItem(id); diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx index 9aa3e6177..3119758ba 100644 --- a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx @@ -19,6 +19,7 @@ import { cn } from "@/lib/utils"; import { Button, Calendar, Input } from "@/components/Inputs"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/Popover"; import { opensearch } from "shared-types"; +import { getNextBusinessDayTimestamp, offsetFromUtc } from "shared-utils"; type Props = Omit< React.HTMLAttributes, @@ -174,7 +175,7 @@ export function FilterableDateRange({ value, onChange, ...props }: Props) { sideOffset={1} > idIsUnique(value), { + .refine(async (value) => !(await itemExists(value)), { message: "According to our records, this SPA ID already exists. Please check the SPA ID and try entering it again.", }); @@ -48,8 +48,7 @@ export const zInitialWaiverNumberSchema = z message: "You can only submit for a state you have access to. If you need to add another state, visit your IDM user profile to request access.", }) - // TODO: update idIsUnique with proper check - .refine(async (value) => idIsUnique(value), { + .refine(async (value) => !(await itemExists(value)), { message: "According to our records, this 1915(b) Waiver Number already exists. Please check the 1915(b) Waiver Number and try entering it again.", }); @@ -64,7 +63,7 @@ export const zRenewalWaiverNumberSchema = z message: "You can only submit for a state you have access to. If you need to add another state, visit your IDM user profile to request access.", }) - .refine(async (value) => idIsUnique(value), { + .refine(async (value) => !(await itemExists(value)), { message: "According to our records, this 1915(b) Waiver Renewal Number already exists. Please check the 1915(b) Waiver Renewal Number and try entering it again.", }); @@ -79,7 +78,7 @@ export const zAmendmentWaiverNumberSchema = z message: "You can only submit for a state you have access to. If you need to add another state, visit your IDM user profile to request access.", }) - .refine(async (value) => idIsUnique(value), { + .refine(async (value) => !(await itemExists(value)), { message: "According to our records, this 1915(b) Waiver Amendment Number already exists. Please check the 1915(b) Waiver Amendment Number and try entering it again.", }); @@ -95,7 +94,7 @@ export const zAmendmentOriginalWaiverNumberSchema = z "You can only submit for a state you have access to. If you need to add another state, visit your IDM user profile to request access.", }) // This should already exist. - .refine(async (value) => !(await idIsUnique(value)), { + .refine(async (value) => await itemExists(value), { message: "According to our records, this 1915(b) Waiver Number does not yet exist. Please check the 1915(b) Waiver Amendment Number and try entering it again.", }) @@ -114,7 +113,7 @@ export const zRenewalOriginalWaiverNumberSchema = z "You can only submit for a state you have access to. If you need to add another state, visit your IDM user profile to request access.", }) // This should already exist - .refine(async (value) => !(await idIsUnique(value)), { + .refine(async (value) => await itemExists(value), { message: "According to our records, this 1915(b) Waiver Number does not yet exist. Please check the 1915(b) Waiver Amendment Number and try entering it again.", })