From e587e1eb725dd96c4f6cb83650fe77e652c7fa91 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 09:55:03 -0500 Subject: [PATCH 01/13] Add cron package --- package.json | 1 + yarn.lock | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/package.json b/package.json index 25465434..c2a2f28c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "body-parser": "^1.18.2", "cors": "^2.8.4", "crawler-user-agents": "https://github.com/monperrus/crawler-user-agents.git#master", + "cron": "^1.3.0", "deep-assign": "^2.0.0", "deepmerge": "^2.1.0", "elasticsearch": "^15.0.0", diff --git a/yarn.lock b/yarn.lock index 96b8409a..e516be1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -963,6 +963,12 @@ cors@^2.8.4: version "1.0.0" resolved "https://github.com/monperrus/crawler-user-agents.git#2e5bafb49a0e8bbf4ec7d6a0a851f6c43725678c" +cron@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/cron/-/cron-1.3.0.tgz#7e459968eaf94e1a445be796ce402166c234659d" + dependencies: + moment-timezone "^0.5.x" + cross-spawn@^4: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -3097,6 +3103,16 @@ mocha@^5.0.0: mkdirp "0.5.1" supports-color "4.4.0" +moment-timezone@^0.5.x: + version "0.5.21" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.21.tgz#3cba247d84492174dbf71de2a9848fa13207b845" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + moment@^2.20.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" From 5b92b3bbc2fa85aa842e9ae56f30542aead360d8 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 09:55:25 -0500 Subject: [PATCH 02/13] Allow setting sendgrid in .env --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2ffc0437..e94f6d16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,8 @@ services: NODE_ENV: development PORT: 8100 REDIS_DSN: redis://redis:6379/0 + SENDGRID_API_KEY: ${SENDGRID_API_KEY} + SENDGRID_FROM: ${SENDGRID_FROM} redis: image: redis:alpine mongo: From 0b250b49f2cc1e24ca0ceb9e5fe65bea45e9bdb1 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 09:56:47 -0500 Subject: [PATCH 03/13] Update contact notifier service to support cron and scheduled notifications --- src/models/campaign-notification.js | 4 + src/schema/campaign-notification.js | 44 +++++++++ src/services/contact-notifier.js | 147 ++++++++++++++++++++++++++-- 3 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 src/models/campaign-notification.js create mode 100644 src/schema/campaign-notification.js diff --git a/src/models/campaign-notification.js b/src/models/campaign-notification.js new file mode 100644 index 00000000..9b4b6141 --- /dev/null +++ b/src/models/campaign-notification.js @@ -0,0 +1,4 @@ +const mongoose = require('../connections/mongoose/instance'); +const schema = require('../schema/campaign-notification'); + +module.exports = mongoose.model('campaign-notification', schema); diff --git a/src/schema/campaign-notification.js b/src/schema/campaign-notification.js new file mode 100644 index 00000000..379da13e --- /dev/null +++ b/src/schema/campaign-notification.js @@ -0,0 +1,44 @@ +const { Schema } = require('mongoose'); +const connection = require('../connections/mongoose/instance'); + +const schema = new Schema({ + campaignId: { + type: Schema.Types.ObjectId, + required: true, + validate: { + async validator(v) { + const doc = await connection.model('campaign').findOne({ _id: v }, { _id: 1 }); + if (doc) return true; + return false; + }, + message: 'No campaign found for ID {VALUE}', + }, + }, + type: { + type: String, + enum: ['Campaign Created', 'Campaign Started', 'Campaign Ended'], + required: true, + }, + status: { + type: String, + enum: ['Pending', 'Sending', 'Sent', 'Errored'], + default: 'Pending', + }, + error: String, + sendAt: { + type: Date, + required: true, + default: () => new Date(), + }, + + to: [String], + cc: [String], + bcc: [String], + + subject: String, + text: String, + html: String, +}, { timestamps: true }); + + +module.exports = schema; diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index c18e9d68..80727cf8 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -4,6 +4,10 @@ const emailTemplates = require('../email-templates'); const ContactRepo = require('../repositories/contact'); const AdvertiserRepo = require('../repositories/advertiser'); const accountService = require('../services/account'); +const CampaignNotification = require('../models/campaign-notification'); +const Campaign = require('../models/campaign'); +const output = require('../output'); +const { CronJob } = require('cron'); module.exports = { @@ -12,7 +16,86 @@ module.exports = { return contacts.map(contact => `${contact.givenName} ${contact.familyName} <${contact.email}>`); }, - async send({ to, subject, html }) { + /** + * Schedules (or updates) a campaign notification + */ + async schedule({ + type, + campaignId, + to, + cc, + subject, + html, + sendAt, + }) { + const criteria = { type, campaignId, status: { $in: ['Pending', 'Sent'] } }; + const notification = await CampaignNotification.findOne(criteria); + if (notification) { + notification.set({ to, cc, sendAt }); + return notification.save(); + } + return CampaignNotification.create({ + type, + campaignId, + to, + cc, + subject, + html, + sendAt, + }); + }, + + init() { + // Run every 10 minutes + const job = new CronJob({ + cronTime: '* */10 * * * *', + onTick: this.check, + runOnInit: true, + context: this, + }); + job.start(); + }, + + async check() { + const criteria = { + status: 'Pending', + sendAt: { $lte: new Date() }, + }; + const count = await CampaignNotification.count(criteria); + output.write(`✉️ ✉️ ✉️ Found ${count} pending notifications.`); + + // Explicitly await in while to ensure that the findOneAndUpdate + // completes before counting if there are more notifications to grab. + + /* eslint-disable no-await-in-loop */ + while (await CampaignNotification.count(criteria) > 0) { + const notification = await CampaignNotification.findOneAndUpdate(criteria, { status: 'Sending' }); + this.handleNotification(notification); + } + /* eslint-enable no-await-in-loop */ + }, + + async handleNotification(notification) { + try { + await this.send(notification); + notification.set('status', 'Sent'); + output.write('✉️ ✉️ ✉️ Successfully sent a notification!'); + } catch (e) { + notification.set('error', e); + notification.set('status', 'Errored'); + notification.save(); + output.write('✉️ ✉️ ✉️ Error encountered sending notification!', e); + } finally { + notification.save(); + } + }, + + async send({ + to, + cc, + subject, + html, + }) { const key = env.SENDGRID_API_KEY; const from = env.SENDGRID_FROM; const bcc = await accountService.setting('bcc'); @@ -22,6 +105,7 @@ module.exports = { const payload = { to, + cc, bcc, from, subject, @@ -32,18 +116,63 @@ module.exports = { return sgMail.send(payload); }, - async sendInternalCampaignCreated({ campaign }) { - const html = await emailTemplates.render('internal/campaign.created', { campaign }); + async scheduleCampaignCreated({ campaignId }) { + const campaign = await Campaign.findOne({ _id: campaignId }); const advertiser = await AdvertiserRepo.findById(campaign.get('advertiserId')); + const html = await emailTemplates.render('campaign.created', { campaign }); const subject = `A new campaign was created for ${advertiser.name}`; - const to = await this.resolveAddresses(campaign.get('notify.internal')); - return this.send({ to, subject, html }); + const to = await this.resolveAddresses(campaign.get('notify.external')); + const cc = await this.resolveAddresses(campaign.get('notify.internal')); + const sendAt = new Date(); + return this.schedule({ + type: 'Campaign Created', + campaignId: campaign.id, + to, + cc, + subject, + html, + sendAt, + }); }, - async sendExternalCampaignCreated({ campaign }) { - const html = await emailTemplates.render('external/campaign.created', { campaign }); - const subject = 'A new campaign was created!'; + async scheduleCampaignStarted({ campaignId }) { + const campaign = await Campaign.findOne({ _id: campaignId }); + const html = await emailTemplates.render('campaign.started', { campaign }); + const subject = `Your campaign "${campaign.name} has started!`; const to = await this.resolveAddresses(campaign.get('notify.external')); - return this.send({ to, subject, html }); + const cc = await this.resolveAddresses(campaign.get('notify.internal')); + const sendAt = campaign.get('criteria.start'); + if (campaign.status !== 'Active' || !sendAt) return Promise.resolve(); + + return this.schedule({ + type: 'Campaign Started', + campaignId: campaign.id, + to, + cc, + subject, + html, + sendAt, + }); + }, + + async scheduleCampaignEnded({ campaignId }) { + const campaign = await Campaign.findOne({ _id: campaignId }); + const html = await emailTemplates.render('campaign.ended', { campaign }); + const subject = `Your campaign "${campaign.name} has ended!`; + const to = await this.resolveAddresses(campaign.get('notify.external')); + const cc = await this.resolveAddresses(campaign.get('notify.internal')); + const sendAt = campaign.get('criteria.end'); + + if (campaign.status !== 'Active' || !sendAt) return Promise.resolve(); + + return this.schedule({ + type: 'Campaign Ended', + campaignId: campaign.id, + to, + cc, + subject, + html, + sendAt, + }); }, }; From db970a10f780297ac6a7bb8ecd4d87f712e1a4f4 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 09:57:05 -0500 Subject: [PATCH 04/13] Add cron job to root --- src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.js b/src/index.js index 4def293d..9d212bab 100644 --- a/src/index.js +++ b/src/index.js @@ -4,10 +4,13 @@ const env = require('./env'); const output = require('./output'); const pkg = require('../package.json'); const { app } = require('./server'); +const contactNotifier = require('./services/contact-notifier'); const { PORT } = env; const server = app.listen(PORT); output.write(`🕸️ 🕸️ 🕸️ Express app '${pkg.name}' listening on port ${PORT}`); +contactNotifier.init(); + module.exports = server; From 28782a7e48b92124bcdefffe10b0d9384e1091db Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 09:57:31 -0500 Subject: [PATCH 05/13] Ensure notifications are updated when campaigns are changed --- src/graph/resolvers/campaign.js | 56 ++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/graph/resolvers/campaign.js b/src/graph/resolvers/campaign.js index 61f38a15..9689deb4 100644 --- a/src/graph/resolvers/campaign.js +++ b/src/graph/resolvers/campaign.js @@ -20,6 +20,12 @@ const getNotifyDefaults = async (advertiserId, user) => { return notify; }; +const updateNotifications = async (campaignId) => { + contactNotifier.scheduleCampaignCreated({ campaignId }); + contactNotifier.scheduleCampaignStarted({ campaignId }); + contactNotifier.scheduleCampaignEnded({ campaignId }); +}; + module.exports = { /** * @@ -111,18 +117,19 @@ module.exports = { payload.criteria = { start: payload.startDate }; payload.notify = await getNotifyDefaults(payload.advertiserId, auth.user); const campaign = await CampaignRepo.create(payload); - contactNotifier.sendInternalCampaignCreated({ campaign }); - contactNotifier.sendExternalCampaignCreated({ campaign }); + await updateNotifications(campaign.id); return campaign; }, /** * */ - updateCampaign: (root, { input }, { auth }) => { + updateCampaign: async (root, { input }, { auth }) => { auth.check(); const { id, payload } = input; - return CampaignRepo.update(id, payload); + const campaign = await CampaignRepo.update(id, payload); + await updateNotifications(id); + return campaign; }, assignCampaignValue: async (root, { input }) => { @@ -130,7 +137,9 @@ module.exports = { const campaign = await Campaign.findById(id); if (!campaign) throw new Error(`Unable to assign field '${field}' to campaign: no record found for id '${id}'`); campaign.set(field, value); - return campaign.save(); + await campaign.save(); + await updateNotifications(id); + return campaign; }, /** @@ -139,7 +148,9 @@ module.exports = { campaignCriteria: async (root, { input }, { auth }) => { auth.check(); const { campaignId, payload } = input; - return CriteriaRepo.setFor(campaignId, payload); + const criteria = await CriteriaRepo.setFor(campaignId, payload); + await updateNotifications(campaignId); + return criteria; }, campaignUrl: async (root, { input }, { auth }) => { @@ -148,16 +159,20 @@ module.exports = { const campaign = await Campaign.findById(campaignId); if (!campaign) throw new Error(`Unable to set campaign URL: no campaign found for '${campaignId}'`); campaign.url = url; - return campaign.save(); + await campaign.save(); + await updateNotifications(campaignId); + return campaign; }, /** * */ - addCampaignCreative: (root, { input }, { auth }) => { + addCampaignCreative: async (root, { input }, { auth }) => { const { campaignId, payload } = input; auth.checkCampaignAccess(campaignId); - return CreativeRepo.createFor(campaignId, payload); + const creative = await CreativeRepo.createFor(campaignId, payload); + await updateNotifications(campaignId); + return creative; }, /** @@ -167,6 +182,7 @@ module.exports = { const { campaignId, creativeId } = input; auth.checkCampaignAccess(campaignId); await CreativeRepo.removeFrom(campaignId, creativeId); + await updateNotifications(campaignId); return 'ok'; }, @@ -176,7 +192,9 @@ module.exports = { campaignCreativeStatus: async (root, { input }, { auth }) => { auth.check(); const { campaignId, creativeId, status } = input; - return CreativeRepo.setStatusFor(campaignId, creativeId, status); + const saved = await CreativeRepo.setStatusFor(campaignId, creativeId, status); + await updateNotifications(campaignId); + return saved; }, /** @@ -186,7 +204,13 @@ module.exports = { const { campaignId, creativeId, payload } = input; auth.checkCampaignAccess(campaignId); const { title, teaser, status } = payload; - return CreativeRepo.updateDetailsFor(campaignId, creativeId, { title, teaser, status }); + const details = await CreativeRepo.updateDetailsFor( + campaignId, + creativeId, + { title, teaser, status }, + ); + await updateNotifications(campaignId); + return details; }, /** @@ -195,16 +219,20 @@ module.exports = { campaignCreativeImage: async (root, { input }, { auth }) => { const { campaignId, creativeId, imageId } = input; auth.checkCampaignAccess(campaignId); - return CreativeRepo.updateImageFor(campaignId, creativeId, imageId); + const image = await CreativeRepo.updateImageFor(campaignId, creativeId, imageId); + await updateNotifications(campaignId); + return image; }, /** * */ - campaignContacts: (root, { input }, { auth }) => { + campaignContacts: async (root, { input }, { auth }) => { auth.check(); const { id, type, contactIds } = input; - return ContactRepo.setContactsFor(Campaign, id, type, contactIds); + const contacts = await ContactRepo.setContactsFor(Campaign, id, type, contactIds); + await updateNotifications(id); + return contacts; }, }, }; From cbc9d0934bb1e7eeb37eed8a861ce30666e3f0fa Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 10:47:35 -0500 Subject: [PATCH 06/13] Make uris virtuals --- src/schema/advertiser.js | 7 +++++++ src/schema/campaign/index.js | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/schema/advertiser.js b/src/schema/advertiser.js index 3fff4304..aa6256a7 100644 --- a/src/schema/advertiser.js +++ b/src/schema/advertiser.js @@ -4,6 +4,7 @@ const notifyPlugin = require('../plugins/notify'); const { applyElasticPlugin, setEntityFields } = require('../elastic/mongoose'); const imagePlugin = require('../plugins/image'); const pushIdPlugin = require('../plugins/push-id'); +const accountService = require('../services/account'); const schema = new Schema({ name: { @@ -38,6 +39,12 @@ schema.pre('save', async function updateCampaigns() { } }); +schema.virtual('portalUri').get(async function getPortalUri() { + const account = await accountService.retrieve(); + const uri = await account.get('uri'); + return `${uri}/app/${this.pushId}`; +}); + schema.plugin(notifyPlugin); setEntityFields(schema, 'name'); diff --git a/src/schema/campaign/index.js b/src/schema/campaign/index.js index 05d273d8..7e520f80 100644 --- a/src/schema/campaign/index.js +++ b/src/schema/campaign/index.js @@ -93,6 +93,27 @@ schema.pre('save', async function setAdvertiserName() { } }); +schema.virtual('portalUri').get(async function getPortalUri() { + const advertiser = await connection.model('advertiser').findOne({ _id: this.advertiserId }, { pushId: 1 }); + const uri = await advertiser.get('portalUri'); + return `${uri}/campaigns/${this.pushId}`; +}); + +schema.virtual('vMaterialCollectUri').get(async function getVMCU() { + const uri = await this.get('portalUri'); + return `${uri}/materials`; +}); + +schema.virtual('vReportSummaryUri').get(async function getVRSU() { + const uri = await this.get('portalUri'); + return `${uri}/report/summary`; +}); + +schema.virtual('vReportCreativeUri').get(async function getVRCU() { + const uri = await this.get('portalUri'); + return `${uri}/report/creative-breakdown`; +}); + schema.plugin(notifyPlugin); schema.plugin(pushIdPlugin, { required: true }); From fce33294481ea33055d5bb4cb3642cc824d3b96b Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 10:50:07 -0500 Subject: [PATCH 07/13] Per hbs project recommendation, await before render rather than await in template --- src/services/contact-notifier.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index 80727cf8..f3e7d6a2 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -119,7 +119,8 @@ module.exports = { async scheduleCampaignCreated({ campaignId }) { const campaign = await Campaign.findOne({ _id: campaignId }); const advertiser = await AdvertiserRepo.findById(campaign.get('advertiserId')); - const html = await emailTemplates.render('campaign.created', { campaign }); + const materialCollectUri = await campaign.get('vMaterialCollectUri') + const html = await emailTemplates.render('campaign.created', { campaign, materialCollectUri }); const subject = `A new campaign was created for ${advertiser.name}`; const to = await this.resolveAddresses(campaign.get('notify.external')); const cc = await this.resolveAddresses(campaign.get('notify.internal')); @@ -157,7 +158,9 @@ module.exports = { async scheduleCampaignEnded({ campaignId }) { const campaign = await Campaign.findOne({ _id: campaignId }); - const html = await emailTemplates.render('campaign.ended', { campaign }); + const reportSummaryUri = await campaign.get('vReportSummaryUri') + const reportCreativeUri = await campaign.get('vReportCreativeUri') + const html = await emailTemplates.render('campaign.ended', { campaign, reportSummaryUri, reportCreativeUri }); const subject = `Your campaign "${campaign.name} has ended!`; const to = await this.resolveAddresses(campaign.get('notify.external')); const cc = await this.resolveAddresses(campaign.get('notify.internal')); From 0b3b65f91fa6d2f4e7a9a41bf90b77962297e869 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 10:50:25 -0500 Subject: [PATCH 08/13] Update templates, remove link-to helper --- src/email-templates/campaign.created.hbs | 3 +++ src/email-templates/campaign.ended.hbs | 6 ++++++ src/email-templates/campaign.started.hbs | 1 + src/email-templates/external/campaign.created.hbs | 3 --- src/email-templates/internal/campaign.created.hbs | 3 --- src/handlebars.js | 2 -- 6 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 src/email-templates/campaign.created.hbs create mode 100644 src/email-templates/campaign.ended.hbs create mode 100644 src/email-templates/campaign.started.hbs delete mode 100644 src/email-templates/external/campaign.created.hbs delete mode 100644 src/email-templates/internal/campaign.created.hbs diff --git a/src/email-templates/campaign.created.hbs b/src/email-templates/campaign.created.hbs new file mode 100644 index 00000000..57381a4a --- /dev/null +++ b/src/email-templates/campaign.created.hbs @@ -0,0 +1,3 @@ +

Campaign "{{ campaign.name }}" was created.

+ +

Click here to submit your creatives: {{ materialCollectUri }}

diff --git a/src/email-templates/campaign.ended.hbs b/src/email-templates/campaign.ended.hbs new file mode 100644 index 00000000..8e15477e --- /dev/null +++ b/src/email-templates/campaign.ended.hbs @@ -0,0 +1,6 @@ +

Campaign "{{ campaign.name }}" has ended.

+ +

Click here to view the summary report: {{ reportSummaryUri }}

+ +

Click here to view the creative report: {{ reportCreativeUri }}

+ diff --git a/src/email-templates/campaign.started.hbs b/src/email-templates/campaign.started.hbs new file mode 100644 index 00000000..741e20a8 --- /dev/null +++ b/src/email-templates/campaign.started.hbs @@ -0,0 +1 @@ +

Campaign "{{ campaign.name }}" has started.

diff --git a/src/email-templates/external/campaign.created.hbs b/src/email-templates/external/campaign.created.hbs deleted file mode 100644 index 9cd580fb..00000000 --- a/src/email-templates/external/campaign.created.hbs +++ /dev/null @@ -1,3 +0,0 @@ -Campaign "{{ campaign.name }}" was created. - -Click here to submit your creatives: {{ link-to account.uri "r/collect" campaign.hash }} diff --git a/src/email-templates/internal/campaign.created.hbs b/src/email-templates/internal/campaign.created.hbs deleted file mode 100644 index e31b5aeb..00000000 --- a/src/email-templates/internal/campaign.created.hbs +++ /dev/null @@ -1,3 +0,0 @@ -Campaign {{ campaign.name }} was created. - -Click here to view the campaign: {{ link-to account.uri "campaign" campaign.hash }} diff --git a/src/handlebars.js b/src/handlebars.js index 62714821..23d5e71d 100644 --- a/src/handlebars.js +++ b/src/handlebars.js @@ -64,6 +64,4 @@ handlebars.registerHelper('build-ua-beacon', (context) => { return new handlebars.SafeString(``); }); -handlebars.registerHelper('link-to', (...parts) => parts.filter(el => typeof el === 'string').join('/')); - module.exports = handlebars; From 1182c44f584f401187a4b4caf833a79d99d1f118 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Tue, 10 Jul 2018 10:59:20 -0500 Subject: [PATCH 09/13] fix lint issues --- src/services/contact-notifier.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index f3e7d6a2..d38dbe28 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -62,6 +62,8 @@ module.exports = { sendAt: { $lte: new Date() }, }; const count = await CampaignNotification.count(criteria); + if (count === 0) return; + output.write(`✉️ ✉️ ✉️ Found ${count} pending notifications.`); // Explicitly await in while to ensure that the findOneAndUpdate @@ -119,7 +121,7 @@ module.exports = { async scheduleCampaignCreated({ campaignId }) { const campaign = await Campaign.findOne({ _id: campaignId }); const advertiser = await AdvertiserRepo.findById(campaign.get('advertiserId')); - const materialCollectUri = await campaign.get('vMaterialCollectUri') + const materialCollectUri = await campaign.get('vMaterialCollectUri'); const html = await emailTemplates.render('campaign.created', { campaign, materialCollectUri }); const subject = `A new campaign was created for ${advertiser.name}`; const to = await this.resolveAddresses(campaign.get('notify.external')); @@ -158,8 +160,8 @@ module.exports = { async scheduleCampaignEnded({ campaignId }) { const campaign = await Campaign.findOne({ _id: campaignId }); - const reportSummaryUri = await campaign.get('vReportSummaryUri') - const reportCreativeUri = await campaign.get('vReportCreativeUri') + const reportSummaryUri = await campaign.get('vReportSummaryUri'); + const reportCreativeUri = await campaign.get('vReportCreativeUri'); const html = await emailTemplates.render('campaign.ended', { campaign, reportSummaryUri, reportCreativeUri }); const subject = `Your campaign "${campaign.name} has ended!`; const to = await this.resolveAddresses(campaign.get('notify.external')); From 001e796d2faab4f8e183a41f40be76f2ff1a95d9 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Mon, 16 Jul 2018 08:30:12 -0500 Subject: [PATCH 10/13] Make subject and html required --- src/schema/campaign-notification.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/schema/campaign-notification.js b/src/schema/campaign-notification.js index 379da13e..247fc3d8 100644 --- a/src/schema/campaign-notification.js +++ b/src/schema/campaign-notification.js @@ -35,9 +35,15 @@ const schema = new Schema({ cc: [String], bcc: [String], - subject: String, + subject: { + type: String, + required: true, + }, text: String, - html: String, + html: { + type: String, + required: true + }, }, { timestamps: true }); From 44f8728e96b6a6613c7ab4822fc5cf50c4d64714 Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Mon, 16 Jul 2018 11:21:21 -0500 Subject: [PATCH 11/13] Use findById --- src/schema/campaign-notification.js | 2 +- src/services/contact-notifier.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schema/campaign-notification.js b/src/schema/campaign-notification.js index 247fc3d8..546dab75 100644 --- a/src/schema/campaign-notification.js +++ b/src/schema/campaign-notification.js @@ -7,7 +7,7 @@ const schema = new Schema({ required: true, validate: { async validator(v) { - const doc = await connection.model('campaign').findOne({ _id: v }, { _id: 1 }); + const doc = await connection.model('campaign').findById(v, { _id: 1 }); if (doc) return true; return false; }, diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index d38dbe28..a34527ce 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -119,7 +119,7 @@ module.exports = { }, async scheduleCampaignCreated({ campaignId }) { - const campaign = await Campaign.findOne({ _id: campaignId }); + const campaign = await Campaign.findById(campaignId); const advertiser = await AdvertiserRepo.findById(campaign.get('advertiserId')); const materialCollectUri = await campaign.get('vMaterialCollectUri'); const html = await emailTemplates.render('campaign.created', { campaign, materialCollectUri }); From b847404e03c379b8b1dfe29d0d8707fd44d3186d Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Mon, 16 Jul 2018 12:10:37 -0500 Subject: [PATCH 12/13] More findBys --- src/services/contact-notifier.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index a34527ce..0e3e41ff 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -139,7 +139,7 @@ module.exports = { }, async scheduleCampaignStarted({ campaignId }) { - const campaign = await Campaign.findOne({ _id: campaignId }); + const campaign = await Campaign.findById(campaignId); const html = await emailTemplates.render('campaign.started', { campaign }); const subject = `Your campaign "${campaign.name} has started!`; const to = await this.resolveAddresses(campaign.get('notify.external')); @@ -159,7 +159,7 @@ module.exports = { }, async scheduleCampaignEnded({ campaignId }) { - const campaign = await Campaign.findOne({ _id: campaignId }); + const campaign = await Campaign.findById(campaignId); const reportSummaryUri = await campaign.get('vReportSummaryUri'); const reportCreativeUri = await campaign.get('vReportCreativeUri'); const html = await emailTemplates.render('campaign.ended', { campaign, reportSummaryUri, reportCreativeUri }); From 9f96f5b3af2c081fd5da0255a44d170cdf9eaedd Mon Sep 17 00:00:00 2001 From: Josh Worden Date: Mon, 16 Jul 2018 12:11:18 -0500 Subject: [PATCH 13/13] cleanup error handling --- src/services/contact-notifier.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/contact-notifier.js b/src/services/contact-notifier.js index 0e3e41ff..4f4b54a8 100644 --- a/src/services/contact-notifier.js +++ b/src/services/contact-notifier.js @@ -83,9 +83,8 @@ module.exports = { notification.set('status', 'Sent'); output.write('✉️ ✉️ ✉️ Successfully sent a notification!'); } catch (e) { - notification.set('error', e); + notification.set('error', e.message); notification.set('status', 'Errored'); - notification.save(); output.write('✉️ ✉️ ✉️ Error encountered sending notification!', e); } finally { notification.save();