Skip to content

Commit

Permalink
feat(export-multiple-campaigns): wip, format and send email after cam…
Browse files Browse the repository at this point in the history
…paign exports process
  • Loading branch information
henryk1229 committed Aug 11, 2023
1 parent 1ed058b commit 46db8cc
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 25 deletions.
10 changes: 10 additions & 0 deletions src/server/api/export-multiple-campaigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { CampaignExportMetaData } from "../lib/templates/export-multiple-campaigns";
import getEmailContent from "../lib/templates/export-multiple-campaigns";

const formatMultipleCampaignExportsEmail = (
campaignExportMetaData: CampaignExportMetaData
) => {
return getEmailContent(campaignExportMetaData);
};

export default formatMultipleCampaignExportsEmail;
3 changes: 1 addition & 2 deletions src/server/lib/templates/export-campaign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ export type ExportURLs = {
campaignOptOutsExportUrl?: string | undefined;
campaignFilteredContactsExportUrl?: string | undefined;
};

interface ExportProps {
exportUrls: ExportURLs;
campaignTitle: string;
}

const ExportCampaign: React.FC<ExportProps> = ({
export const ExportCampaign: React.FC<ExportProps> = ({
exportUrls,
campaignTitle
}) => {
Expand Down
79 changes: 79 additions & 0 deletions src/server/lib/templates/export-multiple-campaigns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import ReactDOMServer from "react-dom/server";

import type { ExportURLs } from "./export-campaign";

export type CampaignExportDetails = {
campaignTitle: string;
exportUrls: ExportURLs | null;
};

export type CampaignExportMetaData = {
[id: string]: CampaignExportDetails;
};

interface Props {
campaignExportMetaData: CampaignExportMetaData;
}

const ExportMultipleCampaigns: React.FC<Props> = ({
campaignExportMetaData
}) => {
const campaignIds = Object.keys(campaignExportMetaData);
return (
<div>
<p>
Your spoke exports are ready! These URLs will be valid for 24 hours.
</p>
{campaignIds.map((campaignId) => {
const { exportUrls, campaignTitle } = campaignExportMetaData[
campaignId
];
// this is checked before rendering the component, but satisfying typescript here
if (!exportUrls) {
throw new Error(
"attempted to render email component without export urls"
);
}
const {
campaignExportUrl,
campaignMessagesExportUrl,
campaignOptOutsExportUrl,
campaignFilteredContactsExportUrl
} = exportUrls;
return (
<>
<p>
{campaignTitle} - ID: {campaignId}
</p>
{campaignExportUrl && <p>Campaign Export: {campaignExportUrl}</p>}
{campaignMessagesExportUrl && (
<p>Campaign Messages Export: {campaignMessagesExportUrl}</p>
)}
{campaignOptOutsExportUrl && (
<p>Campaign OptOuts Export: {campaignOptOutsExportUrl}</p>
)}
{campaignFilteredContactsExportUrl && (
<p>
Campaign Filtered Contacts Export:{" "}
{campaignFilteredContactsExportUrl}
</p>
)}
</>
);
})}
<br />
-- The Spoke Rewired Team
</div>
);
};

const getEmailContent = (campaignExportMetaData: CampaignExportMetaData) => {
const template = (
<ExportMultipleCampaigns campaignExportMetaData={campaignExportMetaData} />
);

return ReactDOMServer.renderToStaticMarkup(template);
};

export default getEmailContent;
61 changes: 38 additions & 23 deletions src/server/tasks/export-multiple-campaigns.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import DateTime from "../../lib/datetime";
import formatMultipleCampaignExportsEmail from "../api/export-multiple-campaigns";
import type { JobRequestRecord } from "../api/types";
import type { CampaignExportMetaData } from "../lib/templates/export-multiple-campaigns";
import sendEmail from "../mail";
import { r } from "../models";
import type { ExportCampaignPayload } from "./export-campaign";
Expand Down Expand Up @@ -56,7 +58,9 @@ export const exportCampaignForBulkOperation: ProgressTask<ExportCampaignPayload>
};

const exportUrls = await processExportData(campaignMetaData, spokeOptions);
helpers.logger.info(`Successfully exported ${campaignId}`);
helpers.logger.info(
`Exported data for campaign: ${campaignTitle} - ID ${campaignId}`
);

// store exportUrls in job_request table
await helpers.updateResult({ message: JSON.stringify(exportUrls) });
Expand All @@ -77,13 +81,14 @@ export const sendEmailForBulkExportOperation: ProgressTask<EmailBulkOperationPay
const { jobRequestRecords, campaignId, requesterId, campaignIds } = payload;

// map campaign id to campaign title and export urls for email composition
const campaignMetaDataMap: Record<string, Record<string, unknown>> = {};
const campaignMetaDataMap: CampaignExportMetaData = {};
for (const id of campaignIds) {
const parsed = parseInt(id, 10);
const { campaignTitle } = await fetchExportData(parsed, requesterId);
campaignMetaDataMap[id] = { campaignTitle, exportUrls: null };
}

const jobRequestIds = jobRequestRecords.map((record) => record.id);
// query job_req table for campaign export urls where status is 100 (exports complete)
const { rows } = await helpers.query(
`
Expand All @@ -92,9 +97,12 @@ export const sendEmailForBulkExportOperation: ProgressTask<EmailBulkOperationPay
where id = ANY ($1)
and status = 100
`,
[jobRequestRecords.map((rec) => rec.id)]
[jobRequestIds]
);

// wait for all campaign exports to process
const exportsStillProcessing = rows.length !== campaignIds.length;

// map query result to campaign id and parse JSON
const campaignExportsMap = rows.map((result) => ({
campaignId: result.campaign_id,
Expand All @@ -103,34 +111,44 @@ export const sendEmailForBulkExportOperation: ProgressTask<EmailBulkOperationPay

// map fetched export urls to campaignMetaData
for (const campaignExport of campaignExportsMap) {
if (!Object.keys(campaignMetaDataMap).includes(campaignExport.campaignId)) {
throw new Error("attempted to index metaData for incorrect campaign");
if (
!Object.keys(campaignMetaDataMap).includes(
campaignExport.campaignId.toString()
)
) {
throw new Error("attempted to store exportUrls for incorrect campaign");
}
campaignMetaDataMap[campaignExport.campaignId].exportUrls =
campaignExport.exportUrls;
}

try {
if (exportsStillProcessing) {
throw new Error("Attempting to send export email before expots process");
}
const campaignIdsString = campaignIds.join(", ");
// get email
const { notificationEmail } = await fetchExportData(
campaignId,
requesterId
);
// TODO - format email content
// trigger retry by throwing if export urls is null for a campaign?
// const exporContent = await formatEmailContent
// trigger retry by throwing if export urls is null for a campaign
const exportContent = formatMultipleCampaignExportsEmail(
campaignMetaDataMap
);
await sendEmail({
to: notificationEmail,
subject: `Export(s) ready for campaign(s) ${campaignIdsString}`
// html: exportContent
subject: `Export(s) ready for campaign(s) ${campaignIdsString}`,
html: exportContent
});
helpers.logger.info(
`Successfully sent export details email for bulk operation`
);
helpers.logger.info(`Successfully sent email for bulk export operation`);
// TODO - restore after debugging
// remove export_campaign job_requests from requests table
// for (const id of jobRequestIds) {
// await helpers.cleanUpJobRequest(id);
// }
} finally {
// TODO - clean up job_request table
helpers.logger.info("Finishing bulk export process");
helpers.logger.info("Successfully completed bulk export operation");
}
};

Expand All @@ -147,30 +165,27 @@ export const addExportMultipleCampaigns = async (
identifier: EXPORT_TASK_IDENTIFIER,
payload: innerPayload,
taskSpec: {
queueName: "export-multiple-campaigns"
queueName: "export-campaigns-for-bulk-operation"
}
});

jobRequestRecords.push(requestRecord);
}

console.log("requests", jobRequestRecords);
// satisfy ProgressJobPayload: campaignId = required
const emailTaskPayload = {
...payload,
campaignId: campaignIds[0],
jobRequestRecords
};
// dispatch a single job to email export urls to client
// dispatch a single job to email exportUrls
await addProgressJob({
identifier: EMAIL_TASK_IDENTIFIER,
payload: emailTaskPayload,
taskSpec: {
queueName: "send-email-after-bulk-export"
queueName: "send-email-after-bulk-export",
priority: 0
}
});
return jobRequestRecords;
};

// desired flow:
// each export gets called as a job
// one email is sent after the exports are processed

0 comments on commit 46db8cc

Please sign in to comment.