Merge pull request #9319 from overleaf/mj-deferred-recurly-email

[web] Use bull queues for deferred cancellation email

GitOrigin-RevId: a104f9940badcffc15f1f237a1cefd5dd912f4e0
This commit is contained in:
Mathias Jakobsen 2022-08-23 10:16:45 +01:00 committed by Copybot
parent 4cfbee15df
commit f5b2cdc3cb
5 changed files with 69 additions and 15 deletions

View file

@ -2,11 +2,13 @@ const { callbackify } = require('util')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const EmailBuilder = require('./EmailBuilder') const EmailBuilder = require('./EmailBuilder')
const EmailSender = require('./EmailSender') const EmailSender = require('./EmailSender')
const Queues = require('../../infrastructure/Queues')
const EMAIL_SETTINGS = Settings.email || {} const EMAIL_SETTINGS = Settings.email || {}
module.exports = { module.exports = {
sendEmail: callbackify(sendEmail), sendEmail: callbackify(sendEmail),
sendDeferredEmail,
promises: { promises: {
sendEmail, sendEmail,
}, },
@ -22,3 +24,11 @@ async function sendEmail(emailType, opts) {
opts.subject = email.subject opts.subject = email.subject
await EmailSender.promises.sendEmail(opts) await EmailSender.promises.sendEmail(opts)
} }
function sendDeferredEmail(emailType, opts, delay) {
Queues.createScheduledJob(
'deferred-emails',
{ data: { emailType, opts } },
delay
)
}

View file

@ -199,20 +199,9 @@ function cancelSubscription(user, callback) {
first_name: user.first_name, first_name: user.first_name,
} }
const ONE_HOUR_IN_MS = 1000 * 60 * 60 const ONE_HOUR_IN_MS = 1000 * 60 * 60
setTimeout( EmailHandler.sendDeferredEmail(
() => 'canceledSubscription',
EmailHandler.sendEmail( emailOpts,
'canceledSubscription',
emailOpts,
err => {
if (err) {
logger.warn(
{ err },
'failed to send confirmation email for subscription cancellation'
)
}
}
),
ONE_HOUR_IN_MS ONE_HOUR_IN_MS
) )
callback() callback()

View file

@ -7,6 +7,9 @@ const {
addOptionalCleanupHandlerBeforeStoppingTraffic, addOptionalCleanupHandlerBeforeStoppingTraffic,
addRequiredCleanupHandlerBeforeDrainingConnections, addRequiredCleanupHandlerBeforeDrainingConnections,
} = require('./GracefulShutdown') } = require('./GracefulShutdown')
const EmailHandler = require('../Features/Email/EmailHandler')
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
function start() { function start() {
if (!Features.hasFeature('saas')) { if (!Features.hasFeature('saas')) {
@ -47,6 +50,19 @@ function start() {
await FeaturesUpdater.promises.refreshFeatures(userId, reason) await FeaturesUpdater.promises.refreshFeatures(userId, reason)
}) })
registerCleanup(refreshFeaturesQueue) registerCleanup(refreshFeaturesQueue)
const deferredEmailsQueue = Queues.getQueue('deferred-emails')
deferredEmailsQueue.process(async job => {
const { emailType, opts } = job.data
try {
await EmailHandler.promises.sendEmail(emailType, opts)
} catch (e) {
const error = OError.tag(e, 'failed to send deferred email')
logger.warn(error)
throw error
}
})
registerCleanup(deferredEmailsQueue)
} }
function registerCleanup(queue) { function registerCleanup(queue) {

View file

@ -20,11 +20,15 @@ describe('EmailHandler', function () {
sendEmail: sinon.stub().resolves(), sendEmail: sinon.stub().resolves(),
}, },
} }
this.Queues = {
createScheduledJob: sinon.stub(),
}
this.EmailHandler = SandboxedModule.require(MODULE_PATH, { this.EmailHandler = SandboxedModule.require(MODULE_PATH, {
requires: { requires: {
'./EmailBuilder': this.EmailBuilder, './EmailBuilder': this.EmailBuilder,
'./EmailSender': this.EmailSender, './EmailSender': this.EmailSender,
'@overleaf/settings': this.Settings, '@overleaf/settings': this.Settings,
'../../infrastructure/Queues': this.Queues,
}, },
}) })
}) })
@ -92,4 +96,27 @@ describe('EmailHandler', function () {
}) })
}) })
}) })
describe('send deferred email', function () {
beforeEach(function () {
this.opts = {
to: 'bob@bob.com',
first_name: 'hello bob',
}
this.emailType = 'canceledSubscription'
this.ONE_HOUR_IN_MS = 1000 * 60 * 60
this.EmailHandler.sendDeferredEmail(
this.emailType,
this.opts,
this.ONE_HOUR_IN_MS
)
})
it('should add a email job to the queue', function () {
expect(this.Queues.createScheduledJob).to.have.been.calledWith(
'deferred-emails',
{ data: { emailType: this.emailType, opts: this.opts } },
this.ONE_HOUR_IN_MS
)
})
})
}) })

View file

@ -110,7 +110,10 @@ describe('SubscriptionHandler', function () {
this.LimitationsManager = { userHasV2Subscription: sinon.stub() } this.LimitationsManager = { userHasV2Subscription: sinon.stub() }
this.EmailHandler = { sendEmail: sinon.stub() } this.EmailHandler = {
sendEmail: sinon.stub(),
sendDeferredEmail: sinon.stub(),
}
this.AnalyticsManager = { recordEventForUser: sinon.stub() } this.AnalyticsManager = { recordEventForUser: sinon.stub() }
@ -405,6 +408,15 @@ describe('SubscriptionHandler', function () {
.calledWith(this.subscription.recurlySubscription_id) .calledWith(this.subscription.recurlySubscription_id)
.should.equal(true) .should.equal(true)
}) })
it('should send the email after 1 hour', function () {
const ONE_HOUR_IN_MS = 1000 * 60 * 60
expect(this.EmailHandler.sendDeferredEmail).to.have.been.calledWith(
'canceledSubscription',
{ to: this.user.email, first_name: this.user.first_name },
ONE_HOUR_IN_MS
)
})
}) })
}) })