Skip to content

Commit

Permalink
Merge pull request #1070 from Enterprise-CMCS/main
Browse files Browse the repository at this point in the history
Release to val
  • Loading branch information
jdinh8124 authored Jan 28, 2025
2 parents cc858d7 + fa054da commit 8aaf84e
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 157 deletions.
Binary file modified bun.lockb
Binary file not shown.
50 changes: 47 additions & 3 deletions lib/lambda/processEmails.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { SESClient, SendEmailCommand, SendEmailCommandInput } from "@aws-sdk/client-ses";
import { EmailAddresses, KafkaEvent, KafkaRecord, Events } from "shared-types";
import {
EmailAddresses,
KafkaEvent,
KafkaRecord,
opensearch,
SEATOOL_STATUS,
Events,
} from "shared-types";
import { decodeBase64WithUtf8, getSecret } from "shared-utils";
import { Handler } from "aws-lambda";
import { getEmailTemplates, getAllStateUsers } from "libs/email";
Expand Down Expand Up @@ -113,11 +120,48 @@ export const handler: Handler<KafkaEvent> = async (event) => {
export async function processRecord(kafkaRecord: KafkaRecord, config: ProcessEmailConfig) {
console.log("processRecord called with kafkaRecord: ", JSON.stringify(kafkaRecord, null, 2));
const { key, value, timestamp } = kafkaRecord;
const id: string = decodeBase64WithUtf8(key);

if (kafkaRecord.topic === "aws.seatool.ksql.onemac.three.agg.State_Plan") {
const safeID = id.replace(/^"|"$/g, "");
const seatoolRecord: Document = {
safeID,
...JSON.parse(decodeBase64WithUtf8(value)),
};
const safeSeatoolRecord = opensearch.main.seatool.transform(safeID).safeParse(seatoolRecord);

if (safeSeatoolRecord.data?.seatoolStatus === SEATOOL_STATUS.WITHDRAWN) {
try {
const item = await os.getItem(config.osDomain, getOsNamespace("main"), safeID);

if (!item?.found || !item?._source) {
console.log(`The package was not found for id: ${id} in mako. Doing nothing.`);
return;
}

const recordToPass = {
timestamp,
...safeSeatoolRecord.data,
submitterName: item._source.submitterName,
submitterEmail: item._source.submitterEmail,
event: "seatool-withdraw",
proposedEffectiveDate: safeSeatoolRecord.data?.proposedDate,
origin: "seatool",
};

await processAndSendEmails(recordToPass as Events[keyof Events], safeID, config);
} catch (error) {
console.error("Error processing record:", JSON.stringify(error, null, 2));
throw error;
}
}
return;
}

if (typeof key !== "string") {
console.log("key is not a string ", JSON.stringify(key, null, 2));
throw new Error("Key is not a string");
}
const id: string = decodeBase64WithUtf8(key);

if (!value) {
console.log("Tombstone detected. Doing nothing for this event");
Expand Down Expand Up @@ -177,8 +221,8 @@ export async function processAndSendEmails(
});

const sec = await getSecret(config.emailAddressLookupSecretName);

const item = await os.getItem(config.osDomain, getOsNamespace("main"), id);

if (!item?.found || !item?._source) {
console.log(`The package was not found for id: ${id}. Doing nothing.`);
return;
Expand Down
2 changes: 2 additions & 0 deletions lib/libs/email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type EmailTemplates = {
"contracting-renewal-state": AuthoritiesWithUserTypesTemplate;
"capitated-renewal-state": AuthoritiesWithUserTypesTemplate;
"respond-to-rai": AuthoritiesWithUserTypesTemplate;
"seatool-withdraw": AuthoritiesWithUserTypesTemplate;
"app-k": AuthoritiesWithUserTypesTemplate;
"upload-subsequent-documents": AuthoritiesWithUserTypesTemplate;
};
Expand All @@ -59,6 +60,7 @@ const emailTemplates: EmailTemplates = {
"contracting-renewal-state": EmailContent.newSubmission,
"capitated-renewal-state": EmailContent.newSubmission,
"respond-to-rai": EmailContent.respondToRai,
"seatool-withdraw": EmailContent.withdrawPackage,
"app-k": EmailContent.newSubmission,
"upload-subsequent-documents": EmailContent.uploadSubsequentDocuments,
};
Expand Down
2 changes: 1 addition & 1 deletion lib/packages/shared-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@18f/us-federal-holidays": "^4.0.0",
"@date-fns/tz": "1.2.0",
"@date-fns/utc": "2.1.0",
"date-fns": "4.1.0",
"date-fns": "^4.1.0",
"shared-types": "*",
"eslint-config-custom-server": "*",
"eslint-config-custom": "*"
Expand Down
28 changes: 14 additions & 14 deletions lib/packages/shared-utils/seatool-date-helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { it, describe, expect } from "vitest";
import { formatSeatoolDate, getNextBusinessDayTimestamp, seaToolFriendlyTimestamp } from ".";
import { formatSeatoolDate, getBusinessDayTimestamp, seaToolFriendlyTimestamp } from ".";
import { format } from "date-fns";

describe("seaToolFriendlyTimestamp", () => {
Expand Down Expand Up @@ -28,41 +28,41 @@ describe("formatSeatoolDate", () => {
});
});

describe("getNextBusinessDayTimestamp", () => {
describe("getBusinessDayTimestamp", () => {
it("identifies weekends", () => {
const testDate = new Date(Date.UTC(2024, 0, 27, 12, 0, 0)); // Saturday, noon, utc
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 29)); // Monday, midnight, utc
const nextDate = getBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 29, 23, 59, 59, 999)); // Monday, midnight, utc
});

it("identifies holidays", () => {
const testDate = new Date(Date.UTC(2024, 0, 15, 12, 0, 0)); // MLK Day, a Monday
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 16)); // Tuesday, midnight, utc
const nextDate = getBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 16, 23, 59, 59, 999)); // Tuesday, midnight, utc
});

it("identifies submissions after 5pm eastern", () => {
const testDate = new Date(Date.UTC(2024, 0, 17, 23, 0, 0)); // Wednesday 11pm utc, Wednesday 6pm eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 18)); // Thursday, midnight, utc
const nextDate = getBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 18, 23, 59, 59, 999)); // Thursday, midnight, utc
});

it("identifies submissions before 5pm eastern", () => {
const testDate = new Date(Date.UTC(2024, 0, 17, 10, 0, 0)); // Wednesday 10am utc, Wednesday 5am eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 17)); // Wednesday, midnight, utc
const nextDate = getBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 17, 23, 59, 59, 999)); // Thursday, midnight, utc
});

it("handles combinations of rule violations", () => {
const testDate = new Date(Date.UTC(2024, 0, 12, 23, 0, 0)); // Friday 11pm utc, Friday 6pm eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
const nextDate = getBusinessDayTimestamp(testDate);
// Submission is after 5pm, Saturday is a weekend, Sunday is a weekend, and Monday is MLK Day
expect(nextDate).toEqual(Date.UTC(2024, 0, 16)); // Tuesday, midnight utc
expect(nextDate).toEqual(Date.UTC(2024, 0, 16, 23, 59, 59, 999)); // Tuesday, midnight utc
});

it("identifies valid business days", () => {
const testDate = new Date(Date.UTC(2024, 0, 9, 15, 0, 0)); // Tuesday 3pm utc, Tuesday 8am eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 9)); // Tuesday, midnight utc
const nextDate = getBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 9, 23, 59, 59, 999)); // Tuesday, midnight utc
});
});
23 changes: 11 additions & 12 deletions lib/packages/shared-utils/seatool-date-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TZDate } from "@date-fns/tz";
import { UTCDate } from "@date-fns/utc";
import { format, startOfDay, isWeekend, addDays } from "date-fns";
import { format, startOfDay, endOfDay, isWeekend, addDays } from "date-fns";
import { isAHoliday } from "@18f/us-federal-holidays";

/**
Expand Down Expand Up @@ -29,26 +29,25 @@ export const formatSeatoolDate = (date?: Date | string): string => {
};

/**
* Returns the epoch timestamp for midnight UTC time for the date provided.
* If no date is provided, it returns the timestamp of midnight UTC time today.
* If the date is after 5pm Eastern time, it returns midnight UTC the next day.
* If the date is on a federal holiday or weekend, it returns midnight UTC of the
* next business day.
* Returns the ISO date string of the current business day.
* If no date is provided, it returns the current business day relative to today.
* If the date is after 5pm Eastern time, it returns the next day.
* If the date is on a federal holiday or weekend, it returns the next business day.
*
* @param date the date object to return the timestamp for
* @returns epoch timestamp for midnight UTC of the date or today, if none provided
* @returns the date string of the current business day relative to the date or today, if none provided
*/
export const getNextBusinessDayTimestamp = (date: Date = new Date()): number => {
export const getBusinessDayTimestamp = (date: Date = new Date()): number => {
// Get the date in Eastern time
const nyDateTime = new TZDate(date.toISOString(), "America/New_York");

// Check if the time is after 5pm Eastern time or if the day is not a business day.
// If any of those are true, check again for the next day.
if (nyDateTime.getHours() >= 17 || isAHoliday(nyDateTime) || isWeekend(nyDateTime)) {
return getNextBusinessDayTimestamp(startOfDay(addDays(nyDateTime, 1)));
return getBusinessDayTimestamp(startOfDay(addDays(nyDateTime, 1)));
}

// If the date is a business day before 5pm Eastern time, return the timestamp
// of midnight UTC on that day
return startOfDay(new UTCDate(date)).getTime();
return endOfDay(
new UTCDate(nyDateTime.getFullYear(), nyDateTime.getMonth(), nyDateTime.getDate()),
).getTime();
};
30 changes: 29 additions & 1 deletion lib/stacks/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export class Email extends cdk.NestedStack {

alarm.addAlarmAction(new cdk.aws_cloudwatch_actions.SnsAction(alarmTopic));

new CfnEventSourceMapping(this, "SinkSESTrigger", {
new CfnEventSourceMapping(this, "SinkSESTriggerOnemac", {
batchSize: 1,
enabled: true,
selfManagedEventSource: {
Expand Down Expand Up @@ -250,6 +250,34 @@ export class Email extends cdk.NestedStack {
},
});

new CfnEventSourceMapping(this, "SinkSESTriggerSEATool", {
batchSize: 1,
enabled: true,
selfManagedEventSource: {
endpoints: {
kafkaBootstrapServers: brokerString.split(","),
},
},
functionName: processEmailsLambda.functionName,
sourceAccessConfigurations: [
...privateSubnets.map((subnet) => ({
type: "VPC_SUBNET",
uri: subnet.subnetId,
})),
{
type: "VPC_SECURITY_GROUP",
uri: `security_group:${lambdaSecurityGroup.securityGroupId}`,
},
],
startingPosition: "LATEST",
topics: [`aws.seatool.ksql.onemac.three.agg.State_Plan`],
destinationConfig: {
onFailure: {
destination: dlq.queueArn,
},
},
});

// Add CloudWatch alarms
new cdk.aws_cloudwatch.Alarm(this, "EmailProcessingErrors", {
metric: processEmailsLambda.metricErrors(),
Expand Down
5 changes: 3 additions & 2 deletions react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"dependencies": {
"@aws-amplify/auth": "^5.4.0",
"@date-fns/utc": "2.1.0",
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@fontsource/open-sans": "^5.0.17",
"@heroicons/react": "^2.0.17",
"@hookform/resolvers": "^3.3.2",
Expand All @@ -36,7 +37,7 @@
"aws-amplify": "^5.3.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"date-fns": "^4.1.0",
"export-to-csv": "^0.2.1",
"file-saver": "^2.0.5",
"framer-motion": "^10.16.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { opensearch } from "shared-types";
import { useFilterDrawerContext } from "../FilterProvider";
import { useLabelMapping } from "@/hooks";
import { UTCDate } from "@date-fns/utc";
import { format } from "date-fns";

export const DATE_FORMAT = "M/d/yyyy";
export interface RenderProp {
Expand Down Expand Up @@ -33,7 +34,9 @@ export const ChipDate: FC<RenderProp> = ({ filter, openDrawer, clearFilter }) =>
const value = filter.value as opensearch.RangeValue;
if (!value?.gte && !value?.lte) return null;
const label = filter?.label ? `${filter.label}: ` : "";
const range = `${new UTCDate(value?.gte || value?.lte).toLocaleDateString()} - ${new Date(value?.lte || value?.gte).toLocaleDateString()}`;
const gte = format(new UTCDate(value?.gte || value?.lte), DATE_FORMAT);
const lte = format(new UTCDate(value?.lte || value?.gte), DATE_FORMAT);
const range = `${gte} - ${lte}`;
return (
<Chip
onChipClick={openDrawer}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ describe("FilterableDateRange", () => {
await user.click(screen.getByText("Pick a date"));
await user.click(screen.getByRole("button", { name: "Today" }));
expect(onChange).toHaveBeenCalledWith({
gte: startOfDay(new UTCDate()).toISOString(),
lte: endOfDay(new UTCDate()).toISOString(),
gte: (startOfDay(new UTCDate()) as UTCDate).toISOString(),
lte: (endOfDay(new UTCDate()) as UTCDate).toISOString(),
});
});

Expand All @@ -98,8 +98,8 @@ describe("FilterableDateRange", () => {
await user.click(screen.getByText("Pick a date"));
await user.click(screen.getByRole("button", { name: "Last 7 Days" }));
expect(onChange).toHaveBeenCalledWith({
gte: startOfDay(sub(new UTCDate(), { days: 6 })).toISOString(),
lte: endOfDay(new UTCDate()).toISOString(),
gte: (startOfDay(sub(new UTCDate(), { days: 6 })) as UTCDate).toISOString(),
lte: (endOfDay(new UTCDate()) as UTCDate).toISOString(),
});
});

Expand All @@ -108,9 +108,10 @@ describe("FilterableDateRange", () => {
render(<FilterableDateRange value={{ gte: undefined, lte: undefined }} onChange={onChange} />);
await user.click(screen.getByText("Pick a date"));
await user.click(screen.getByRole("button", { name: "Month To Date" }));

expect(onChange).toHaveBeenCalledWith({
gte: startOfDay(startOfMonth(new UTCDate())).toISOString(),
lte: endOfDay(new UTCDate()).toISOString(),
gte: (startOfDay(startOfMonth(new UTCDate())) as UTCDate).toISOString(),
lte: (endOfDay(new UTCDate()) as UTCDate).toISOString(),
});
});

Expand All @@ -120,8 +121,8 @@ describe("FilterableDateRange", () => {
await user.click(screen.getByText("Pick a date"));
await user.click(screen.getByRole("button", { name: "Month To Date" }));
expect(onChange).toHaveBeenCalledWith({
gte: startOfDay(startOfQuarter(new UTCDate())).toISOString(),
lte: endOfDay(new UTCDate()).toISOString(),
gte: (startOfDay(startOfQuarter(new UTCDate())) as UTCDate).toISOString(),
lte: (endOfDay(new UTCDate()) as UTCDate).toISOString(),
});
});

Expand All @@ -136,14 +137,15 @@ describe("FilterableDateRange", () => {
await user.click(firstDay);
const selectedDate = startOfMonth(new UTCDate());
expect(onChange).toHaveBeenCalledWith({
gte: startOfDay(selectedDate).toISOString(),
lte: endOfDay(selectedDate).toISOString(),
gte: (startOfDay(selectedDate) as UTCDate).toISOString(),
lte: (endOfDay(selectedDate) as UTCDate).toISOString(),
});
});

it("should handle the first day set to the month and clicking today", async () => {
const user = userEvent.setup();
const firstDay = startOfMonth(new UTCDate());
const firstDay = startOfMonth(new UTCDate()) as UTCDate;
console.log({ firstDay, formatted: format(firstDay, DATE_FORMAT) });
render(
<FilterableDateRange
value={{ gte: format(firstDay, DATE_FORMAT), lte: undefined }}
Expand All @@ -152,7 +154,7 @@ describe("FilterableDateRange", () => {
);
await user.click(screen.getByText(format(firstDay, DATE_DISPLAY_FORMAT)));
const pickers = screen.getAllByRole("grid");
const todayDate = new UTCDate();
const todayDate = startOfDay(new UTCDate());
const todayDay = within(pickers[0])
.getAllByRole("gridcell", { name: `${getDate(todayDate)}` })
.find((day) => !day.getAttribute("disabled"));
Expand All @@ -162,8 +164,8 @@ describe("FilterableDateRange", () => {
await user.click(todayDay);

expect(onChange).toHaveBeenLastCalledWith({
gte: startOfDay(firstDay).toISOString(),
lte: endOfDay(todayDate).toISOString(),
gte: (startOfDay(firstDay) as UTCDate).toISOString(),
lte: (endOfDay(todayDate) as UTCDate).toISOString(),
});
}
});
Expand Down
Loading

0 comments on commit 8aaf84e

Please sign in to comment.