mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 10:53:34 +03:00
Added email notification for new donations
fixes https://github.com/TryGhost/Product/issues/3692
This commit is contained in:
parent
97580a3cd8
commit
5462bc0a96
@ -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) => {
|
||||
|
@ -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() {
|
||||
|
@ -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 */
|
||||
|
64
ghost/staff-service/lib/email-templates/donation.hbs
Normal file
64
ghost/staff-service/lib/email-templates/donation.hbs
Normal 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;"> </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;">Don’t 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;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
13
ghost/staff-service/lib/email-templates/donation.txt.js
Normal file
13
ghost/staff-service/lib/email-templates/donation.txt.js
Normal 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}.
|
||||
`;
|
||||
};
|
@ -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', {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user