Skip to content

Commit

Permalink
Merge branch 'main' into jmo-vue-recommended-core-views
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffsmohan authored Mar 20, 2024
2 parents 7acb666 + f0e42ff commit b8c93d0
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,20 @@ describe('audit report generation', () => {
'Project Expenditure Category Group': '2-Negative Economic Impacts',
'Project ID': '4',
}];
const headers = audit_report.createHeadersProjectSummariesV2(projects);
const headers = audit_report.sortHeadersWithDates(projects,
[
'Capital Expenditure Amount',
'Project Description',
'Project Expenditure Category',
'Project Expenditure Category Group',
'Project ID',
],
[
'Total Aggregate Expenditures',
'Total Aggregate Obligations',
'Total Expenditures for Awards Greater or Equal to $50k',
'Total Obligations for Awards Greater or Equal to $50k',
]);
const headersExpected = [
'Project ID',
'Project Description',
Expand Down
2 changes: 2 additions & 0 deletions packages/server/__tests__/email/email.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe('Email sender', () => {
sinon.replace(emailService, 'getTransport', sinon.fake.returns({ sendEmail: sendFake }));

email.deliverEmail({
fromName: 'Foo',
toAddress: '[email protected]',
ccAddress: '[email protected]',
emailHTML: '<p>foo</p>',
Expand All @@ -203,6 +204,7 @@ describe('Email sender', () => {

expect(sendFake.calledOnce).to.equal(true);
expect(sendFake.firstCall.args).to.deep.equal([{
fromName: 'Foo',
toAddress: '[email protected]',
ccAddress: '[email protected]',
subject: 'test foo email',
Expand Down
62 changes: 36 additions & 26 deletions packages/server/src/arpa_reporter/lib/audit-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,16 +398,16 @@ async function createReportsGroupedBySubAward(periodId, tenantId, dateFormat = R
// - the initial value for each column in this row is zero
subAwardReportingPeriodIds.forEach((id) => {
const endDate = endDatesByReportingPeriodId[id];
row[`${endDate} Awards > 50000 SubAward Amount`] = 0;
row[`${endDate} Awards > 50000 SubAward Expenditure`] = 0;
row[`${endDate} Awards > 50000 SubAward Amount (Obligation)`] = 0;
row[`${endDate} Awards > 50000 SubAward Current Expenditure Amount`] = 0;
});

// Sum the total value of each initialized column from the corresponding subtotal
// provided by each subAward record
subAwardRecords.forEach((record) => {
const endDate = endDatesByReportingPeriodId[record.upload.reporting_period_id];
row[`${endDate} Awards > 50000 SubAward Amount`] += (record.content.Award_Amount__c || 0);
row[`${endDate} Awards > 50000 SubAward Expenditure`] += (record.content.Expenditure_Amount__c || 0);
row[`${endDate} Awards > 50000 SubAward Amount (Obligation)`] += (record.content.Award_Amount__c || 0);
row[`${endDate} Awards > 50000 SubAward Current Expenditure Amount`] += (record.content.Expenditure_Amount__c || 0);
});

subAwardLogger.fields.subAward.totalColumns = Object.keys(row).length;
Expand Down Expand Up @@ -465,8 +465,8 @@ async function createKpiDataGroupedByProject(periodId, tenantId, logger = log) {
* The headers are split into the date and non-date headers.
* The non-date headers come first with an ordering, then the date headers.
*/
function createHeadersProjectSummariesV2(projectSummaryGroupedByProject) {
const keys = Array.from(new Set(projectSummaryGroupedByProject.map(Object.keys).flat()));
function sortHeadersWithDates(data, expectedOrderWithoutDate, expectedOrderWithDate) {
const keys = Array.from(new Set(data.map(Object.keys).flat()));
// split up by date and not date
const withDate = keys.filter((x) => REPORTING_DATE_REGEX.exec(x));
const withoutDate = keys.filter((x) => REPORTING_DATE_REGEX.exec(x) == null);
Expand All @@ -486,23 +486,6 @@ function createHeadersProjectSummariesV2(projectSummaryGroupedByProject) {
return x;
}, {});

const expectedOrderWithoutDate = [
'Project ID',
'Project Description',
'Project Expenditure Category Group',
'Project Expenditure Category',
'Capital Expenditure Amount',
];

const expectedOrderWithDate = [
'Total Aggregate Obligations',
'Total Aggregate Expenditures',
'Total Obligations for Awards Greater or Equal to $50k',
'Total Expenditures for Awards Greater or Equal to $50k',
'Total Obligations for Aggregate Awards < $50k',
'Total Expenditures for Aggregate Awards < $50k',
];

// first add the properly ordered non-date headers,
// then add the headers sorted by the header group then the date
const headers = [
Expand All @@ -514,6 +497,7 @@ function createHeadersProjectSummariesV2(projectSummaryGroupedByProject) {
.map((date) => `${date.format(REPORTING_DATE_FORMAT)} ${x[0]}`)).flat(),
];

debugger;
return headers;
}

Expand Down Expand Up @@ -579,10 +563,36 @@ async function generate(requestHost, tenantId, periodId) {
const sheet1 = jsonToSheet(obligations, 'Obligations & Expenditures');
const sheet2 = jsonToSheet(projectSummaries, 'Project Summaries');
const sheet3 = jsonToSheet(projectSummaryGroupedByProject, 'Project Summaries V2', {
header: createHeadersProjectSummariesV2(projectSummaryGroupedByProject),
header: sortHeadersWithDates(
projectSummaryGroupedByProject,
[
'Project ID',
'Project Description',
'Project Expenditure Category Group',
'Project Expenditure Category',
'Capital Expenditure Amount',
],
[
'Total Aggregate Obligations',
'Total Aggregate Expenditures',
'Total Obligations for Awards Greater or Equal to $50k',
'Total Expenditures for Awards Greater or Equal to $50k',
'Total Obligations for Aggregate Awards < $50k',
'Total Expenditures for Aggregate Awards < $50k',
],
),
});
// FIXME need to sort
const sheet4 = jsonToSheet(projectSummaryGroupedBySubAward, 'SubAward Summaries');
const sheet4 = jsonToSheet(projectSummaryGroupedBySubAward, 'SubAward Summaries', {
header: sortHeadersWithDates(
projectSummaryGroupedBySubAward,
['SubAward ID'],
[
'Awards > 50000 SubAward Amount (Obligation)',
'Awards > 50000 SubAward Current Expenditure Amount',
],
),
});
const sheet5 = jsonToSheet(KPIDataGroupedByProject, 'KPI');
log.info('finished building sheets from aggregated data');

Expand Down Expand Up @@ -704,7 +714,7 @@ module.exports = {
generateAndSendEmail,
processSQSMessageRequest,
sendEmailWithLink,
createHeadersProjectSummariesV2,
sortHeadersWithDates,

// export for testing
getRecordsByProject,
Expand Down
41 changes: 36 additions & 5 deletions packages/server/src/lib/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ const ASYNC_REPORT_TYPES = {
const HELPDESK_EMAIL = '[email protected]';

async function deliverEmail({
fromName,
toAddress,
ccAddress,
emailHTML,
emailPlain,
subject,
}) {
return emailService.getTransport().sendEmail({
fromName,
toAddress,
ccAddress,
subject,
Expand All @@ -33,16 +35,38 @@ async function deliverEmail({
});
}

function buildBaseUrlSafe() {
const baseUrl = new URL(process.env.WEBSITE_DOMAIN);
baseUrl.searchParams.set('utm_source', 'usdr-grants');
baseUrl.searchParams.set('utm_medium', 'email');
return baseUrl.toString();
}

/**
* Adds the base email HTML around the email body HTML. Specifically, adds the USDR logo header,
* footer, title, preheader, etc.
*
* @param {string} emailHTML - Rendered email body HTML
* @param {object} brandDetails - Options to control how the base branding is rendered
* @param {string} brandDetails.tool_name - Name of the product triggering the email, rendered
* underneath the USDR logo
* @param {string} brandDetails.title - Rendered as the HTML <title> (most email programs ignore)
* @param {string} brandDetails.preheader - Preview text for the email (most email programs
* render this, often truncated, after the subject line in your inbox)
* @param {string} brandDetails.notifications_url - URL where the user can manage notification settings
*/
function addBaseBranding(emailHTML, brandDetails) {
const { tool_name, title, notifications_url } = brandDetails;
const {
tool_name, title, preheader, notifications_url,
} = brandDetails;
const baseBrandedTemplate = fileSystem.readFileSync(path.join(__dirname, '../static/email_templates/base.html'));
const brandedHTML = mustache.render(baseBrandedTemplate.toString(), {
tool_name,
title,
webview_available: false, // Preheader and webview are not setup for Grant notification email.
// preheader: 'Test preheader',
preheader,
// webview_url: 'http://localhost:8080',
usdr_url: 'http://usdigitalresponse.org',
base_url_safe: buildBaseUrlSafe(),
usdr_logo_url: 'https://grants.usdigitalresponse.org/usdr_logo_transparent.png',
notifications_url,
}, {
Expand Down Expand Up @@ -298,7 +322,7 @@ async function buildDigestBody({ name, openDate, matchedGrants }) {
}

async function sendGrantDigest({
name, matchedGrants, recipients, openDate,
name, matchedGrants, matchedGrantsTotal, recipients, openDate,
}) {
console.log(`${name} is subscribed for notifications on ${openDate}`);

Expand All @@ -313,10 +337,14 @@ async function sendGrantDigest({
}

const formattedBody = await buildDigestBody({ name, openDate, matchedGrants });
const preheader = typeof matchedGrantsTotal === 'number' && matchedGrantsTotal > 0
? `You have ${Intl.NumberFormat('en-US', { useGrouping: true }).format(matchedGrantsTotal)} new ${matchedGrantsTotal > 1 ? 'grants' : 'grant'} to review!`
: 'You have new grants to review!';

const emailHTML = module.exports.addBaseBranding(formattedBody, {
tool_name: 'Federal Grant Finder',
title: 'New Grants Digest',
preheader,
notifications_url: (process.env.ENABLE_MY_PROFILE === 'true') ? `${process.env.WEBSITE_DOMAIN}/my-profile` : `${process.env.WEBSITE_DOMAIN}/grants?manageSettings=true`,
});

Expand All @@ -327,10 +355,11 @@ async function sendGrantDigest({
recipients.forEach(
(recipient) => inputs.push(
{
fromName: 'USDR Federal Grant Finder',
toAddress: recipient.trim(),
emailHTML,
emailPlain,
subject: `New Grants published for ${name}`,
subject: `New Grants Published for ${name}`,
},
),
);
Expand All @@ -357,6 +386,7 @@ async function getAndSendGrantForSavedSearch({
return sendGrantDigest({
name: userSavedSearch.name,
matchedGrants: response.data,
matchedGrantsTotal: response.pagination.total,
recipients: [userSavedSearch.email],
openDate,
});
Expand Down Expand Up @@ -399,6 +429,7 @@ async function buildAndSendGrantDigest() {
agencies.forEach((agency) => inputs.push({
name: agency.name,
matchedGrants: agency.matched_grants,
matchedGrantsTotal: agency.matched_grants.length,
recipients: agency.recipients,
openDate,
}));
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/lib/email/email-nodemailer.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ async function sendEmail(message) {

const transport = createTransport();
const params = {
from: process.env.NODEMAILER_EMAIL, // sender address
from: {
name: message.fromName, // If not provided, undefined value is ignored just fine by nodemailer
address: process.env.NODEMAILER_EMAIL,
},
to: message.toAddress, // list of receivers e.g. '[email protected], [email protected]'
subject: message.subject,
// text: 'Hello world?', // plain text body
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/routes/agencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ router.get('/sendDigestEmail', requireUSDRSuperAdminUser, async (req, res) => {
await email.sendGrantDigest({
name: agency[0].name,
matchedGrants: agency[0].matched_grants,
matchedGrantsTotal: agency[0].matched_grants?.length,
recipients: agency[0].recipients,
});
} catch (e) {
Expand Down
52 changes: 25 additions & 27 deletions packages/server/src/static/email_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,15 @@

<body
style="width:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:'open sans', 'helvetica neue', helvetica, arial, sans-serif;padding:0;Margin:0">
{{# preheader }}
<div style="display: none; max-height: 0px; overflow: hidden;">
{{ preheader }}
</div>
<!-- Insert &#847;&zwnj;&nbsp; after hidden preview text to add space and avoid pulling in other email content -->
<div style="display: none; max-height: 0px; overflow: hidden;">
&#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy;
</div>
{{/ preheader }}
<div class="es-wrapper-color" style="background-color:#F6F6F6">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
Expand Down Expand Up @@ -391,38 +400,27 @@
<td style="padding:0;Margin:0;background-size:cover;background-color:transparent"
bgcolor="#DDEFFC" align="center">
<table class="es-header-body"
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;margin-top:50px;background-color:#ddeffc;"
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;margin-top:50px;background-color:#ddeffc;width:640px;"
cellspacing="0" cellpadding="0" align="center">
<tr style="border-collapse:collapse">
<td align="left"
style="Margin:0;padding-top:32px;padding-left:20px;padding-right:20px;padding-bottom:32px">
<td style="Margin:0;padding-top:32px;padding-left:20px;padding-right:20px;padding-bottom:32px" align="center">
<table cellspacing="0" cellpadding="0"
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
<tr style="border-collapse:collapse">
<td valign="top" align="center"
style="padding:0;Margin:0;width:600px">
<table width="100%" cellspacing="0" cellpadding="0"
role="presentation"
style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
<tr style="border-collapse:collapse">
<td align="center"
style="padding:0;Margin:0;font-size:0px">
<a href="{{usdr_url}}" target="_blank"
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#B7BDC9;font-size:20px"><img
src="{{usdr_logo_url}}"
style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic"
alt="Logo" title="Logo" width="167"></a>
</td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0">
<p
style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;line-height:21px;color:#000000;font-size:14px">
<strong>{{tool_name}}</strong>
</p>
</td>
</tr>
</table>
<td align="left" style="padding:0;Margin:0">
<a href="{{& base_url_safe }}" target="_blank" style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#B7BDC9;font-size:20px">
<img
src="{{usdr_logo_url}}"
style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic"
alt="Logo" title="Logo" width="167">
</a>
</td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0">
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;line-height:21px;color:#000000;font-size:14px">
<strong>{{tool_name}}</strong>
</p>
</td>
</tr>
</table>
Expand Down
Loading

0 comments on commit b8c93d0

Please sign in to comment.