Added email notification for new donations

fixes https://github.com/TryGhost/Product/issues/3692
This commit is contained in:
Simon Backx 2023-08-07 15:36:59 +02:00 committed by Simon Backx
parent 97580a3cd8
commit 5462bc0a96
8 changed files with 162 additions and 1 deletions

View File

@ -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) => {

View File

@ -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() {

View File

@ -241,6 +241,57 @@ class StaffServiceEmails {
}
}
/**
*
* @param {object} eventData
* @param {import('@tryghost/donations').DonationPaymentEvent} eventData.donationPaymentEvent
*
* @returns {Promise<void>}
*/
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 */

View File

@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>💸 Received a donation of {{donation.amount}} from {{donation.name}}</title>
{{> styles}}
</head>
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
<!-- START CENTERED CONTAINER -->
{{#> preview}}
{{#*inline "content"}}
{{donation.amount}} from {{donation.name}}
{{/inline}}
{{/preview}}
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Congratulations!</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">You received a <span style="font-weight: bold; color: #15212A;">donation of {{donation.amount}}</span> from {{donation.name}} ({{donation.email}}).</p>
</td>
</tr>
<!-- START FOOTER -->
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{toEmail}}</a></p>
</td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">Dont want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">here</a>.</p>
</td>
</tr>
<!-- END FOOTER -->
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED CONTAINER -->
</div>
</td>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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}.
`;
};

View File

@ -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', {

View File

@ -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,

View File

@ -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;
}