diff --git a/ghost/core/core/server/models/user.js b/ghost/core/core/server/models/user.js index 6ba91f84bd..171da1ba24 100644 --- a/ghost/core/core/server/models/user.js +++ b/ghost/core/core/server/models/user.js @@ -510,6 +510,8 @@ User = ghostBookshelf.Model.extend({ filter += '+mention_notifications:true'; } else if (type === 'milestone-received') { filter += '+milestone_notifications:true'; + } else if (type === 'donation') { + filter += '+donation_notifications:true'; } const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']}); return this.findAll(updatedOptions).then((users) => { diff --git a/ghost/core/core/server/services/stripe/service.js b/ghost/core/core/server/services/stripe/service.js index 59663ec670..0bbeb24db7 100644 --- a/ghost/core/core/server/services/stripe/service.js +++ b/ghost/core/core/server/services/stripe/service.js @@ -10,6 +10,7 @@ const models = require('../../models'); const {getConfig} = require('./config'); const settingsHelpers = require('../settings-helpers'); const donationService = require('../donations'); +const staffService = require('../staff'); async function configureApi() { const cfg = getConfig({settingsHelpers, config, urlUtils}); @@ -56,7 +57,8 @@ module.exports = new StripeService({ }]); } }, - donationService + donationService, + staffService }); module.exports.init = async function init() { diff --git a/ghost/staff-service/lib/StaffServiceEmails.js b/ghost/staff-service/lib/StaffServiceEmails.js index afae6f0434..b0303b3649 100644 --- a/ghost/staff-service/lib/StaffServiceEmails.js +++ b/ghost/staff-service/lib/StaffServiceEmails.js @@ -241,6 +241,57 @@ class StaffServiceEmails { } } + /** + * + * @param {object} eventData + * @param {import('@tryghost/donations').DonationPaymentEvent} eventData.donationPaymentEvent + * + * @returns {Promise} + */ + async notifyDonationReceived({donationPaymentEvent}) { + const emailPromises = []; + const users = await this.models.User.getEmailAlertUsers('donation'); + const formattedAmount = this.getFormattedAmount({currency: donationPaymentEvent.currency, amount: donationPaymentEvent.amount / 100}); + + const subject = `💸 Received a donation of ${formattedAmount} from ${donationPaymentEvent.name ?? donationPaymentEvent.email}`; + + for (const user of users) { + const to = user.email; + + const templateData = { + siteTitle: this.settingsCache.get('title'), + siteUrl: this.urlUtils.getSiteUrl(), + siteDomain: this.siteDomain, + fromEmail: this.fromEmailAddress, + toEmail: to, + adminUrl: this.urlUtils.urlFor('admin', true), + staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`), + donation: { + name: donationPaymentEvent.name ?? donationPaymentEvent.email, + email: donationPaymentEvent.email, + amount: formattedAmount + } + }; + + const {html, text} = await this.renderEmailTemplate('donation', templateData); + + emailPromises.push(await this.sendMail({ + to, + subject, + html, + text + })); + } + + const results = await Promise.allSettled(emailPromises); + + for (const result of results) { + if (result.status === 'rejected') { + this.logging.warn(result?.reason); + } + } + } + // Utils /** @private */ diff --git a/ghost/staff-service/lib/email-templates/donation.hbs b/ghost/staff-service/lib/email-templates/donation.hbs new file mode 100644 index 0000000000..c031076562 --- /dev/null +++ b/ghost/staff-service/lib/email-templates/donation.hbs @@ -0,0 +1,64 @@ + + + + + + 💸 Received a donation of {{donation.amount}} from {{donation.name}} + {{> styles}} + + + + + + + + +
  +
+ + + {{#> preview}} + {{#*inline "content"}} + {{donation.amount}} from {{donation.name}} + {{/inline}} + {{/preview}} + + + + + + + + + +
+ + + + + + + + + + + + + + +
+

Congratulations!

+

You received a donation of {{donation.amount}} from {{donation.name}} ({{donation.email}}).

+
+

This message was sent from {{siteDomain}} to {{toEmail}}

+
+

Don’t want to receive these emails? Manage your preferences here.

+
+
+ + + +
+
 
+ + diff --git a/ghost/staff-service/lib/email-templates/donation.txt.js b/ghost/staff-service/lib/email-templates/donation.txt.js new file mode 100644 index 0000000000..26189b8302 --- /dev/null +++ b/ghost/staff-service/lib/email-templates/donation.txt.js @@ -0,0 +1,13 @@ +module.exports = function (data) { + // Be careful when you indent the email, because whitespaces are visible in emails! + return ` +Congratulations! + +You received a donation of ${data.donation.amount} from "${data.donation.name}". + +--- + +Sent to ${data.toEmail} from ${data.siteDomain}. +If you would no longer like to receive these notifications you can adjust your settings at ${data.staffUrl}. + `; +}; diff --git a/ghost/staff-service/test/staff-service.test.js b/ghost/staff-service/test/staff-service.test.js index 93b0798e98..ca4879a995 100644 --- a/ghost/staff-service/test/staff-service.test.js +++ b/ghost/staff-service/test/staff-service.test.js @@ -909,6 +909,27 @@ describe('StaffService', function () { }); }); + describe('notifyDonationReceived', function () { + it('send donation email', async function () { + const donationPaymentEvent = { + amount: 1500, + currency: 'eur', + name: 'Simon', + email: 'simon@example.com' + }; + + await service.emails.notifyDonationReceived({donationPaymentEvent}); + + getEmailAlertUsersStub.calledWith('donation').should.be.true(); + + mailStub.calledOnce.should.be.true(); + + mailStub.calledWith( + sinon.match.has('html', sinon.match('donation of €15.00 from Simon')) + ).should.be.true(); + }); + }); + describe('renderText for webmentions', function () { it('renders plaintext report for mentions', async function () { const textTemplate = await service.emails.renderText('mention-report', { diff --git a/ghost/stripe/lib/StripeService.js b/ghost/stripe/lib/StripeService.js index 0f0383e114..7728b9c9f5 100644 --- a/ghost/stripe/lib/StripeService.js +++ b/ghost/stripe/lib/StripeService.js @@ -9,6 +9,7 @@ module.exports = class StripeService { constructor({ membersService, donationService, + staffService, StripeWebhook, models }) { @@ -36,6 +37,9 @@ module.exports = class StripeService { get donationRepository() { return donationService.repository; }, + get staffServiceEmails() { + return staffService.api.emails; + }, sendSignupEmail(email){ return membersService.api.sendEmailWithMagicLink({ email, diff --git a/ghost/stripe/lib/WebhookController.js b/ghost/stripe/lib/WebhookController.js index 188297562f..8a43a3edbe 100644 --- a/ghost/stripe/lib/WebhookController.js +++ b/ghost/stripe/lib/WebhookController.js @@ -12,6 +12,7 @@ module.exports = class WebhookController { * @param {any} deps.memberRepository * @param {any} deps.productRepository * @param {import('@tryghost/donations').DonationRepository} deps.donationRepository + * @param {any} deps.staffServiceEmails * @param {any} deps.sendSignupEmail */ constructor(deps) { @@ -135,6 +136,9 @@ module.exports = class WebhookController { }); await this.deps.donationRepository.save(data); + await this.deps.staffServiceEmails.notifyDonationReceived({ + donationPaymentEvent: data + }); } return; }