Skip to content

Commit

Permalink
feat:(allstate): Add allstate users to withdraw package state users (#…
Browse files Browse the repository at this point in the history
…1088)

* Update

* Update

* add test email to cc in dev

* add logic

* add check for valid email

* add update item in withdraw email logic

* feat(lambda): Create split SPAs 2 (#1085)

* update id broken

* ad dlog

* log

* log

* adjust logic

* hm

* why

* log getpackage

* logs

* revert?

* log os

* log

* log

* client.get stuck

* fix

* fix

* log error

* what

* revert

* revert

* wip

* validate id

* zod error

* await

* fix query

* fix query again

* fix query

* try keyword

* try

* query

* test

* log

* last index

* logic

* test

* cleanup

* edit names and sinkmainprocessor

* weird

* log

* fix

* huh

* log

* bug

* admin change schema

* missing in schema

* idToBeUpdated

* add changelog type

* zod typo?

* facepalm

* admin package activity

* log changelog pushing, add success response

* debug changelog

* log docs

* change split spa changelog logic

* add tests wip

* clean up and fix error bubbling

* reference baseschema id shape

* fix import errors and tests wip

* test after refactor bug

* test fix

* fix

* rm logs and update comments

* change body parsing, update timestamps, mod admin schema

* fix timestamp

* hm

* was it this line

* test change

* revert

* log not incrementing

* log fix

* log hits

* m not showing in hits

* look for m

* query size?

* syntax fix

* rm logs and test admin schema change again

* revert

* import error

* import again

* revert

* remove

* topic name not defined

* rm unnecessary packageId check and wip tests

* reduce query size

* reduce query size?

* change order of pushing?

* remove query size

* consistent date time

* update test fix error rejection

* add regexp to query type for split spas

* modify mocked os search func to accomodate for split spas

* sorry adding split spas into mock items

* put query size back

* tears.

* update test to use requestContext

* revert packageExists

* correct timestamp and add mockEvent for upload sub docs

* remove example json field

* mod example json

* add logic

* add additional types

* Revert "Merge remote-tracking branch 'origin' into allstate-james"

This reverts commit a4ed237, reversing
changes made to 4c1ea45.

---------

Co-authored-by: tiffanyvu <[email protected]>

* sequential processing

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* Update sinkMainProcessors.test.ts

* add brians suggestion

* logs

* add undefined email logic

* cleanup

* Update email-components.tsx

---------

Co-authored-by: James Dinh <[email protected]>
Co-authored-by: James Dinh <[email protected]>
Co-authored-by: tiffanyvu <[email protected]>
  • Loading branch information
4 people authored Jan 30, 2025
1 parent 44ad660 commit b6c09a7
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 46 deletions.
79 changes: 57 additions & 22 deletions lib/lambda/processEmails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { getEmailTemplates, getAllStateUsers } from "libs/email";
import * as os from "libs/opensearch-lib";
import { EMAIL_CONFIG, getCpocEmail, getSrtEmails } from "libs/email/content/email-components";
import { htmlToText, HtmlToTextOptions } from "html-to-text";
import pLimit from "p-limit";
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { getOsNamespace } from "libs/utils";

Expand All @@ -37,6 +36,13 @@ interface ProcessEmailConfig {
isDev: boolean;
}

interface EmailTemplate {
to: string[];
cc?: string[];
subject: string;
body: string;
}

export const handler: Handler<KafkaEvent> = async (event) => {
const requiredEnvVars = [
"emailAddressLookupSecretName",
Expand Down Expand Up @@ -140,6 +146,11 @@ export async function processRecord(kafkaRecord: KafkaRecord, config: ProcessEma
return;
}

if (item._source.withdrawEmailSent) {
console.log("Withdraw email previously sent");
return;
}

const recordToPass = {
timestamp,
...safeSeatoolRecord.data,
Expand All @@ -151,6 +162,18 @@ export async function processRecord(kafkaRecord: KafkaRecord, config: ProcessEma
};

await processAndSendEmails(recordToPass as Events[keyof Events], safeID, config);

const indexObject = {
index: getOsNamespace("main"),
id: safeID,
body: {
doc: {
withdrawEmailSent: true,
},
},
};

await os.updateData(config.osDomain, indexObject);
} catch (error) {
console.error("Error processing record:", JSON.stringify(error, null, 2));
throw error;
Expand Down Expand Up @@ -210,7 +233,7 @@ export async function processAndSendEmails(

if (!templates) {
console.log(
`The kafka record has an event type that does not have email support. event: ${record.event}. Doing nothing.`,
`The kafka record has an event type that does not have email support. event: ${record.event}. Doing nothing.`,
);
return;
}
Expand Down Expand Up @@ -251,9 +274,12 @@ export async function processAndSendEmails(
};

console.log("Template variables:", JSON.stringify(templateVariables, null, 2));
const limit = pLimit(5); // Limit concurrent emails
const sendEmailPromises = templates.map((template) =>
limit(async () => {

const results = [];

// Process templates sequentially
for (const template of templates) {
try {
const filledTemplate = await template(templateVariables);
validateEmailTemplate(filledTemplate);
const params = createEmailParams(
Expand All @@ -262,34 +288,43 @@ export async function processAndSendEmails(
config.applicationEndpointUrl,
config.isDev,
);
try {
await sendEmail(params, config.region);
} catch (error) {
console.error("Error sending email:", error);
throw error;
}
}),
);

try {
await Promise.all(sendEmailPromises);
} catch (error) {
console.error("Error sending emails:", error);
throw error;
const result = await sendEmail(params, config.region);
results.push({ success: true, result });
console.log(`Successfully sent email for template: ${JSON.stringify(result)}`);
} catch (error) {
console.error("Error processing template:", error);
results.push({ success: false, error });
// Continue with next template instead of throwing
}
}

// Log final results
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;

console.log(`Email sending complete. Success: ${successCount}, Failures: ${failureCount}`);

// If all emails failed, throw an error to trigger retry/DLQ logic
if (failureCount === templates.length) {
throw new Error(`All ${failureCount} email(s) failed to send`);
}

return results;
}

export function createEmailParams(
filledTemplate: any,
filledTemplate: EmailTemplate,
sourceEmail: string,
baseUrl: string,
isDev: boolean,
): SendEmailCommandInput {
const params = {
const params: SendEmailCommandInput = {
Destination: {
ToAddresses: filledTemplate.to,
CcAddresses: filledTemplate.cc,
BccAddresses: isDev ? [`State Submitter <${EMAIL_CONFIG.DEV_EMAIL}>`] : [], // this is so emails can be tested in dev as they should have the correct recipients but be blind copied on all emails on dev
CcAddresses: isDev
? [...(filledTemplate.cc || []), `State Submitter <${EMAIL_CONFIG.DEV_EMAIL}>`]
: filledTemplate.cc,
},
Message: {
Body: {
Expand Down
41 changes: 32 additions & 9 deletions lib/libs/email/content/email-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,17 @@ const WithdrawRAI: React.FC<WithdrawRAIProps> = ({ variables, relatedEvent }) =>

const getCpocEmail = (item?: os.main.ItemResult): string[] => {
try {
if (item?._source?.leadAnalystEmail && item?._source?.leadAnalystName) {
const cpocEmail = `${item._source.leadAnalystName} <${item._source.leadAnalystEmail}>`;
return [cpocEmail];
const email = item?._source?.leadAnalystEmail;
const name = item?._source?.leadAnalystName;

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!email || !emailRegex.test(email)) {
console.error(`Invalid or missing email for item: ${JSON.stringify(item?._source, null, 2)}`);
return [];
}
return [];

return [`${name} <${email}>`];
} catch (e) {
console.error("Error getting CPOC email", e);
return [];
Expand All @@ -321,12 +327,29 @@ const getCpocEmail = (item?: os.main.ItemResult): string[] => {

const getSrtEmails = (item?: os.main.ItemResult): string[] => {
try {
if (item?._source?.reviewTeam && item._source.reviewTeam.length > 0) {
return item._source.reviewTeam.map(
(reviewer: { name: string; email: string }) => `${reviewer.name} <${reviewer.email}>`,
);
const reviewTeam = item?._source?.reviewTeam;

// Email validation regex
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!reviewTeam || reviewTeam.length === 0) {
return [];
}
return [];

return reviewTeam
.map((reviewer: { name: string; email: string }) => {
const { name, email } = reviewer;

if (!email || !emailRegex.test(email)) {
console.error(
`Invalid or missing email for reviewer: ${JSON.stringify(reviewer, null, 2)}`,
);
return null;
}

return `${name} <${email}>`;
})
.filter((email): email is string => email !== null);
} catch (e) {
console.error("Error getting SRT emails", e);
return [];
Expand Down
8 changes: 6 additions & 2 deletions lib/libs/email/content/withdrawPackage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export const withdrawPackage: AuthoritiesWithUserTypesTemplate = {
variables: Events["WithdrawPackage"] & CommonEmailVariables & { emails: EmailAddresses },
) => {
return {
to: [`${variables.submitterName} <${variables.submitterEmail}>`], // TODO: change to ALL state users
to: variables.allStateUsersEmails || [
`${variables.submitterName} <${variables.submitterEmail}>`,
],
subject: `Waiver Package ${variables.id} Withdraw Request`,
body: await render(<WaiverStateEmail variables={variables} />),
};
Expand All @@ -97,7 +99,9 @@ export const withdrawPackage: AuthoritiesWithUserTypesTemplate = {
variables: Events["WithdrawPackage"] & CommonEmailVariables & { emails: EmailAddresses },
) => {
return {
to: [`${variables.submitterName} <${variables.submitterEmail}>`], // TODO: change to ALL state users
to: variables.allStateUsersEmails || [
`${variables.submitterName} <${variables.submitterEmail}>`,
],
subject: `Waiver Package ${variables.id} Withdraw Request`,
body: await render(<WaiverStateEmail variables={variables} />),
};
Expand Down
53 changes: 40 additions & 13 deletions lib/libs/email/getAllStateUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ListUsersCommand,
ListUsersCommandInput,
ListUsersCommandOutput,
AttributeType,
} from "@aws-sdk/client-cognito-identity-provider";

export type StateUser = {
Expand All @@ -12,6 +13,14 @@ export type StateUser = {
formattedEmailAddress: string;
};

type CognitoUserAttributes = {
email?: string;
given_name?: string;
family_name?: string;
"custom:state"?: string;
[key: string]: string | undefined;
};

export const getAllStateUsers = async ({
userPoolId,
state,
Expand All @@ -33,26 +42,44 @@ export const getAllStateUsers = async ({
if (!response.Users || response.Users.length === 0) {
return [];
}

const filteredStateUsers = response.Users.filter((user) => {
const stateAttribute = user.Attributes?.find((attr) => attr.Name === "custom:state");
const stateAttribute = user.Attributes?.find(
(attr): attr is AttributeType => attr.Name === "custom:state" && attr.Value !== undefined,
);
return stateAttribute?.Value?.split(",").includes(state);
}).map((user) => {
const attributes = user.Attributes?.reduce(
(acc, attr) => {
acc[attr.Name as any] = attr.Value;
return acc;
},
{} as Record<string, string | undefined>,
);
const attributes = user.Attributes?.reduce<CognitoUserAttributes>((acc, attr) => {
if (attr.Name && attr.Value) {
acc[attr.Name] = attr.Value;
}
return acc;
}, {});

// Skip users without valid email components
if (!attributes?.email) {
console.error(`No email found for user: ${JSON.stringify(user, null, 2)}`);
return null;
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(attributes.email)) {
console.error(`Invalid email format for user: ${attributes.email}`);
return null;
}

const formattedEmailAddress = `${attributes.given_name} ${attributes.family_name} <${attributes.email}>`;

return {
firstName: attributes?.["given_name"],
lastName: attributes?.["family_name"],
email: attributes?.["email"],
formattedEmailAddress: `${attributes?.["given_name"]} ${attributes?.["family_name"]} <${attributes?.["email"]}>`,
firstName: attributes.given_name ?? "",
lastName: attributes.family_name ?? "",
email: attributes.email,
formattedEmailAddress,
};
});

return filteredStateUsers as StateUser[];
return filteredStateUsers.filter((user): user is StateUser => user !== null);
} catch (error) {
console.error("Error fetching users:", error);
throw new Error("Error fetching users");
Expand Down
1 change: 1 addition & 0 deletions lib/packages/shared-types/opensearch/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type Document = AppkDocument &
changeMade?: string;
idToBeUpdated?: string;
mockEvent?: string;
withdrawEmailSent?: boolean;
};

export type Response = Res<Document>;
Expand Down

0 comments on commit b6c09a7

Please sign in to comment.