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

1566 Send confirmation email to new subscriber #1571

Merged
merged 6 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@sentry/nestjs": "^8.35.0",
"@sentry/profiling-node": "^8.35.0",
"@sentry/wizard": "^3.34.2",
"@sendgrid/mail": "^8.1.4",
"@turf/bbox": "^6.0.1",
"@turf/buffer": "^5.1.5",
"@turf/helpers": "^6.1.4",
Expand Down
11 changes: 9 additions & 2 deletions server/src/subscriber/subscriber.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SubscriberService } from "./subscriber.service";
import { Request } from "express";
import validateEmail from "../_utils/validate-email";
import * as Sentry from "@sentry/nestjs";
import crypto from 'crypto';

const PAUSE_BETWEEN_CHECKS = 30000;
const CHECKS_BEFORE_FAIL = 10;
Expand Down Expand Up @@ -59,7 +60,8 @@ export class SubscriberController {
}

// If we have reached this point, the user either doesn't exist or isn't signed up for the list
const addToQueue = await this.subscriberService.create(request.body.email, this.list, this.sendgridEnvironment, request.body.subscriptions, response)
const id = crypto.randomUUID();
const addToQueue = await this.subscriberService.create(request.body.email, this.list, this.sendgridEnvironment, request.body.subscriptions, id, response)

if(addToQueue.isError) {
response.status(addToQueue.code).send({errors: addToQueue.response.body.errors})
Expand All @@ -78,7 +80,12 @@ export class SubscriberController {

// Now we keep checking to make sure the import was successful
const importConfirmation = await this.subscriberService.checkCreate(addToQueue.result[1]["job_id"], response, 0, CHECKS_BEFORE_FAIL, PAUSE_BETWEEN_CHECKS, errorInfo);


// Send the confirmation email
if (!importConfirmation.isError) {
await this.subscriberService.sendConfirmationEmail(request.body.email, this.sendgridEnvironment, request.body.subscriptions, id);
}

return;

}
Expand Down
3 changes: 2 additions & 1 deletion server/src/subscriber/subscriber.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { SubscriberController } from "./subscriber.controller";
import { SubscriberService } from "./subscriber.service";
import { ConfigModule } from "../config/config.module";
import { Client } from "@sendgrid/client";
import { MailService } from "@sendgrid/mail";

@Module({
imports: [ConfigModule],
providers: [SubscriberService, Client],
providers: [SubscriberService, Client, MailService],
exports: [SubscriberService],
controllers: [SubscriberController]
})
Expand Down
76 changes: 71 additions & 5 deletions server/src/subscriber/subscriber.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Res } from "@nestjs/common";
import { ConfigService } from "../config/config.service";
import { Client } from "@sendgrid/client";
import { MailService } from "@sendgrid/mail";
import crypto from 'crypto';
import * as Sentry from "@sentry/nestjs";

Expand All @@ -12,6 +13,7 @@ const validCustomFieldValues = [1] as const;
export type CustomFieldValueTuple = typeof validCustomFieldValues;
type CustomFieldValue = CustomFieldValueTuple[number];

type ValidSubscriptionSet = Record<CustomFieldName, CustomFieldValue>;

type HttpMethod = 'get'|'GET'|'post'|'POST'|'put'|'PUT'|'patch'|'PATCH'|'delete'|'DELETE';

Expand All @@ -26,10 +28,12 @@ export class SubscriberService {
sendgridEnvironmentIdVariable = "";
constructor(
private readonly config: ConfigService,
private client: Client
private client: Client,
private mailer: MailService
) {
this.client.setApiKey(this.config.get("SENDGRID_API_KEY"));
this.sendgridEnvironmentIdVariable = `zap_${this.config.get("SENDGRID_ENVIRONMENT")}_id`;
this.mailer.setApiKey(this.config.get("SENDGRID_API_KEY"));
}

/**
Expand Down Expand Up @@ -61,10 +65,10 @@ export class SubscriberService {
* @param {string} list - The email list to which we will add the user
* @param {string} environment - Staging or production
* @param {object} subscriptions - The CDs the user is subscribing to
* @param {string} id - The id needed for confirmation
* @returns {object}
*/
async create(email: string, list: string, environment: string, subscriptions: object, @Res() response) {
const id = crypto.randomUUID();
async create(email: string, list: string, environment: string, subscriptions: object, id: string, @Res() response) {
var custom_fields = Object.entries(subscriptions).reduce((acc, curr) => ({...acc, [`zap_${environment}_${curr[0]}`]: curr[1]}), {[`zap_${environment}_confirmed`]: 0})
custom_fields[this.sendgridEnvironmentIdVariable] = id;

Expand Down Expand Up @@ -123,7 +127,6 @@ export class SubscriberService {

const confirmationRequest = {
url: `/v3/marketing/contacts/imports/${importId}`,
// method:<HttpMethod> 'GET',
method:<HttpMethod> 'GET',
}

Expand All @@ -145,12 +148,42 @@ export class SubscriberService {
}
}

/**
* Send the user an email requesting signup confirmation.
* @param {string} email - The user's email address
* @param {string} environment - Staging or production
* @param {object} subscriptions - The CDs the user is subscribing to
* @param {string} id - The id needed for confirmation
* @returns {object}
*/
async sendConfirmationEmail(email: string, environment: string, subscriptions: ValidSubscriptionSet, id: string) {
// https://github.com/sendgrid/sendgrid-nodejs/blob/main/docs/use-cases/transactional-templates.md
const msg = {
to: email,
from: '[email protected]', // Your verified sender
templateId: 'd-3684647ef2b242d8947b65b20497baa0',
dynamicTemplateData: {
"id": id,
"domain": environment === "production" ? "zap.planning.nyc.gov" : "zap-staging.planninglabs.nyc",
"subscriptions": this.convertSubscriptionsToHandlebars(subscriptions)
}
}
this.mailer.send(msg)
.then((response) => {
return {isError: false, statusCode: response[0].statusCode}
})
.catch((error) => {
console.error(error)
return {isError: true, ...error}
})
}

/**
* Validate a list of subscriptions.
* @param {object} subscriptions - The subscriptions to validate.
* @returns {boolean}
*/
validateSubscriptions(subscriptions: object) {
validateSubscriptions(subscriptions: ValidSubscriptionSet) {
if (!subscriptions)
return false;

Expand Down Expand Up @@ -183,5 +216,38 @@ export class SubscriberService {
return validCustomFieldValues.includes(value as CustomFieldValue);
}

/**
* Convert the uploaded subscriptions object into a format for Handlebars to use in the confirmation email
* @param {object} subscriptions - The set of CDs the user is subscribing to
* @returns {boolean}
*/
private convertSubscriptionsToHandlebars(subscriptions: ValidSubscriptionSet) {
var handlebars = { "citywide": false, "boroughs": [] }
TylerMatteo marked this conversation as resolved.
Show resolved Hide resolved
const boros = {
"K": "Brooklyn",
"X": "Bronx",
"M": "Manhattan",
"Q": "Queens",
"R": "Staten Island"
}
for (const [key, value] of Object.entries(subscriptions)) {
if (value === 1) {
if (key === "CW") {
handlebars.citywide = true;
} else if (boros[key[0]]) {
const i = handlebars.boroughs.findIndex((boro) => boro.name === boros[key[0]]);
if (i === -1) {
handlebars.boroughs.push({
"name": boros[key[0]],
"communityBoards": [parseInt(key.slice(-2))]
})
} else {
handlebars.boroughs[i]["communityBoards"].push(parseInt(key.slice(-2)))
}
}
}
}
return handlebars;
}

}
17 changes: 16 additions & 1 deletion server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,14 @@
"@sendgrid/helpers" "^8.0.0"
axios "^1.6.8"

"@sendgrid/client@^8.1.4":
version "8.1.4"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-8.1.4.tgz#4db39e49d8ed732169d73b5d5c94d2b11907970d"
integrity sha512-VxZoQ82MpxmjSXLR3ZAE2OWxvQIW2k2G24UeRPr/SYX8HqWLV/8UBN15T2WmjjnEb5XSmFImTJOKDzzSeKr9YQ==
dependencies:
"@sendgrid/helpers" "^8.0.0"
axios "^1.7.4"

"@sendgrid/helpers@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-8.0.0.tgz#f74bf9743bacafe4c8573be46166130c604c0fc1"
Expand Down Expand Up @@ -1449,6 +1457,13 @@
xcode "3.0.1"
xml-js "^1.6.11"
yargs "^16.2.0"
"@sendgrid/mail@^8.1.4":
version "8.1.4"
resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-8.1.4.tgz#0ba72906685eae1a1ef990cca31e962f1ece6928"
integrity sha512-MUpIZykD9ARie8LElYCqbcBhGGMaA/E6I7fEcG7Hc2An26QJyLtwOaKQ3taGp8xO8BICPJrSKuYV4bDeAJKFGQ==
dependencies:
"@sendgrid/client" "^8.1.4"
"@sendgrid/helpers" "^8.0.0"

"@sinclair/typebox@^0.27.8":
version "0.27.8"
Expand Down Expand Up @@ -3666,7 +3681,7 @@ axios@^0.21.1:
dependencies:
follow-redirects "^1.10.0"

axios@^1.6.8:
axios@^1.6.8, axios@^1.7.4:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
Expand Down
Loading