Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:(allstate): Add allstate users to withdraw package state users #1088

Merged
merged 26 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3b7204c
Update
benjaminpaige Jan 29, 2025
5aa8f84
Update
benjaminpaige Jan 29, 2025
7cfa330
Merge branch 'main' into allstate
benjaminpaige Jan 29, 2025
58c0d92
add test email to cc in dev
benjaminpaige Jan 29, 2025
ae9ccb1
add logic
jdinh8124 Jan 29, 2025
ff2a716
Merge branch 'allstate' of https://github.com/Enterprise-CMCS/macpro-…
jdinh8124 Jan 29, 2025
ccd6038
add check for valid email
benjaminpaige Jan 29, 2025
e929ee7
Merge branch 'main' into allstate
jdinh8124 Jan 29, 2025
c32049e
add update item in withdraw email logic
jdinh8124 Jan 29, 2025
7870242
Merge branch 'allstate' of https://github.com/Enterprise-CMCS/macpro-…
jdinh8124 Jan 29, 2025
f46175b
sequential processing
benjaminpaige Jan 29, 2025
d97d875
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
024d6c9
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
0f6966c
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
49833d9
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
426d58b
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
cd685cc
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
798e703
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
aeb932d
Update sinkMainProcessors.test.ts
jdinh8124 Jan 29, 2025
bfbf4aa
add brians suggestion
jdinh8124 Jan 29, 2025
620a799
logs
jdinh8124 Jan 30, 2025
415919c
add undefined email logic
jdinh8124 Jan 30, 2025
9d02097
cleanup
jdinh8124 Jan 30, 2025
6168600
Merge branch 'main' into allstate
jdinh8124 Jan 30, 2025
a332242
Update email-components.tsx
jdinh8124 Jan 30, 2025
a05f854
Merge branch 'allstate' of https://github.com/Enterprise-CMCS/macpro-…
jdinh8124 Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading