mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +03:00
Added MilestoneCreatedEvent
to staff service notifications (#16307)
refs https://www.notion.so/ghost/Marketing-Milestone-email-campaigns-1d2c9dee3cfa4029863edb16092ad5c4?pvs=4 - Added MilestoneCreatedEvent subscription to staff-service incl. first handling - Logs information for now instead of actually sending an email
This commit is contained in:
parent
fed2cb2675
commit
e0331bbfcf
@ -508,6 +508,8 @@ User = ghostBookshelf.Model.extend({
|
|||||||
filter += '+paid_subscription_canceled_notification:true';
|
filter += '+paid_subscription_canceled_notification:true';
|
||||||
} else if (type === 'mention-received') {
|
} else if (type === 'mention-received') {
|
||||||
filter += '+mention_notifications:true';
|
filter += '+mention_notifications:true';
|
||||||
|
} else if (type === 'milestone-received') {
|
||||||
|
filter += '+milestone_notifications:true';
|
||||||
}
|
}
|
||||||
const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
|
const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
|
||||||
return this.findAll(updatedOptions).then((users) => {
|
return this.findAll(updatedOptions).then((users) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
const assert = require('assert');
|
||||||
const staffService = require('../../../../../core/server/services/staff');
|
const staffService = require('../../../../../core/server/services/staff');
|
||||||
|
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
const DomainEvents = require('@tryghost/domain-events');
|
||||||
@ -7,15 +7,18 @@ const {mockManager} = require('../../../../utils/e2e-framework');
|
|||||||
const models = require('../../../../../core/server/models');
|
const models = require('../../../../../core/server/models');
|
||||||
|
|
||||||
const {SubscriptionCancelledEvent, MemberCreatedEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
const {SubscriptionCancelledEvent, MemberCreatedEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
||||||
|
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
||||||
|
|
||||||
describe('Staff Service:', function () {
|
describe('Staff Service:', function () {
|
||||||
|
let userModelStub;
|
||||||
|
|
||||||
before(function () {
|
before(function () {
|
||||||
models.init();
|
models.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockManager.mockMail();
|
mockManager.mockMail();
|
||||||
sinon.stub(models.User, 'getEmailAlertUsers').resolves([{
|
userModelStub = sinon.stub(models.User, 'getEmailAlertUsers').resolves([{
|
||||||
email: 'owner@ghost.org',
|
email: 'owner@ghost.org',
|
||||||
slug: 'ghost'
|
slug: 'ghost'
|
||||||
}]);
|
}]);
|
||||||
@ -226,4 +229,37 @@ describe('Staff Service:', function () {
|
|||||||
mockManager.assert.sentEmailCount(0);
|
mockManager.assert.sentEmailCount(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('milestone created event:', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockLabsEnabled('milestoneEmails');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
sinon.restore();
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs when milestone event is handled', async function () {
|
||||||
|
await staffService.init();
|
||||||
|
DomainEvents.dispatch(MilestoneCreatedEvent.create({
|
||||||
|
milestone: {
|
||||||
|
type: 'arr',
|
||||||
|
currency: 'usd',
|
||||||
|
name: 'arr-100-usd',
|
||||||
|
value: 100,
|
||||||
|
createdAt: new Date(),
|
||||||
|
emailSentAt: new Date()
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
currentARR: 105
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wait for the dispatched events (because this happens async)
|
||||||
|
await DomainEvents.allSettled();
|
||||||
|
const [userCalls] = userModelStub.args[0];
|
||||||
|
assert.equal(userCalls, ['milestone-received']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -194,6 +194,24 @@ class StaffServiceEmails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} eventData
|
||||||
|
* @param {object} eventData.milestone
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async notifyMilestoneReceived({milestone}) {
|
||||||
|
const users = await this.models.User.getEmailAlertUsers('milestone-received');
|
||||||
|
|
||||||
|
// TODO: send email with correct templates
|
||||||
|
for (const user of users) {
|
||||||
|
const to = user.email;
|
||||||
|
|
||||||
|
this.logging.info(`Will send email to ${to} for ${milestone.type} / ${milestone.value} milestone.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
@ -227,7 +245,7 @@ class StaffServiceEmails {
|
|||||||
/** @private */
|
/** @private */
|
||||||
getFormattedAmount({amount = 0, currency}) {
|
getFormattedAmount({amount = 0, currency}) {
|
||||||
if (!currency) {
|
if (!currency) {
|
||||||
return '';
|
return amount > 0 ? Intl.NumberFormat().format(amount) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Intl.NumberFormat('en', {
|
return Intl.NumberFormat('en', {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
||||||
const {MentionCreatedEvent} = require('@tryghost/webmentions');
|
const {MentionCreatedEvent} = require('@tryghost/webmentions');
|
||||||
|
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
||||||
|
|
||||||
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
|
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
|
||||||
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
|
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
|
||||||
@ -80,6 +81,11 @@ class StaffService {
|
|||||||
if (type === MentionCreatedEvent && event.data.mention && this.labs.isSet('webmentions')) {
|
if (type === MentionCreatedEvent && event.data.mention && this.labs.isSet('webmentions')) {
|
||||||
await this.emails.notifyMentionReceived(event.data);
|
await this.emails.notifyMentionReceived(event.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === MilestoneCreatedEvent && event.data.milestone && this.labs.isSet('milestoneEmails')) {
|
||||||
|
await this.emails.notifyMilestoneReceived(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
if (!['api', 'member'].includes(event.data.source)) {
|
if (!['api', 'member'].includes(event.data.source)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -146,6 +152,15 @@ class StaffService {
|
|||||||
this.logging.error(e, `Failed to notify webmention`);
|
this.logging.error(e, `Failed to notify webmention`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Trigger email when a new milestone is reached
|
||||||
|
this.DomainEvents.subscribe(MilestoneCreatedEvent, async (event) => {
|
||||||
|
try {
|
||||||
|
await this.handleEvent(MilestoneCreatedEvent, event);
|
||||||
|
} catch (e) {
|
||||||
|
this.logging.error(e, `Failed to notify milestone`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
||||||
const {MentionCreatedEvent} = require('@tryghost/webmentions');
|
const {MentionCreatedEvent} = require('@tryghost/webmentions');
|
||||||
|
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
||||||
|
|
||||||
require('./utils');
|
require('./utils');
|
||||||
const StaffService = require('../lib/staff-service');
|
const StaffService = require('../index');
|
||||||
|
|
||||||
function testCommonMailData({mailStub, getEmailAlertUsersStub}) {
|
function testCommonMailData({mailStub, getEmailAlertUsersStub}) {
|
||||||
getEmailAlertUsersStub.calledWith(
|
getEmailAlertUsersStub.calledWith(
|
||||||
@ -108,6 +109,7 @@ describe('StaffService', function () {
|
|||||||
|
|
||||||
describe('email notifications:', function () {
|
describe('email notifications:', function () {
|
||||||
let mailStub;
|
let mailStub;
|
||||||
|
let loggingInfoStub;
|
||||||
let subscribeStub;
|
let subscribeStub;
|
||||||
let getEmailAlertUsersStub;
|
let getEmailAlertUsersStub;
|
||||||
let service;
|
let service;
|
||||||
@ -147,6 +149,7 @@ describe('StaffService', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
loggingInfoStub = sinon.stub().resolves();
|
||||||
mailStub = sinon.stub().resolves();
|
mailStub = sinon.stub().resolves();
|
||||||
subscribeStub = sinon.stub().resolves();
|
subscribeStub = sinon.stub().resolves();
|
||||||
getEmailAlertUsersStub = sinon.stub().resolves([{
|
getEmailAlertUsersStub = sinon.stub().resolves([{
|
||||||
@ -155,6 +158,7 @@ describe('StaffService', function () {
|
|||||||
}]);
|
}]);
|
||||||
service = new StaffService({
|
service = new StaffService({
|
||||||
logging: {
|
logging: {
|
||||||
|
info: loggingInfoStub,
|
||||||
warn: () => {},
|
warn: () => {},
|
||||||
error: () => {}
|
error: () => {}
|
||||||
},
|
},
|
||||||
@ -182,16 +186,19 @@ describe('StaffService', function () {
|
|||||||
describe('subscribeEvents', function () {
|
describe('subscribeEvents', function () {
|
||||||
it('subscribes to events', async function () {
|
it('subscribes to events', async function () {
|
||||||
service.subscribeEvents();
|
service.subscribeEvents();
|
||||||
subscribeStub.callCount.should.eql(4);
|
subscribeStub.callCount.should.eql(5);
|
||||||
subscribeStub.calledWith(SubscriptionActivatedEvent).should.be.true();
|
subscribeStub.calledWith(SubscriptionActivatedEvent).should.be.true();
|
||||||
subscribeStub.calledWith(SubscriptionCancelledEvent).should.be.true();
|
subscribeStub.calledWith(SubscriptionCancelledEvent).should.be.true();
|
||||||
subscribeStub.calledWith(MemberCreatedEvent).should.be.true();
|
subscribeStub.calledWith(MemberCreatedEvent).should.be.true();
|
||||||
subscribeStub.calledWith(MentionCreatedEvent).should.be.true();
|
subscribeStub.calledWith(MentionCreatedEvent).should.be.true();
|
||||||
|
subscribeStub.calledWith(MilestoneCreatedEvent).should.be.true();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleEvent', function () {
|
describe('handleEvent', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
loggingInfoStub = sinon.stub().resolves();
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
User: {
|
User: {
|
||||||
getEmailAlertUsers: sinon.stub().resolves([{
|
getEmailAlertUsers: sinon.stub().resolves([{
|
||||||
@ -255,6 +262,7 @@ describe('StaffService', function () {
|
|||||||
|
|
||||||
service = new StaffService({
|
service = new StaffService({
|
||||||
logging: {
|
logging: {
|
||||||
|
info: loggingInfoStub,
|
||||||
warn: () => {},
|
warn: () => {},
|
||||||
error: () => {}
|
error: () => {}
|
||||||
},
|
},
|
||||||
@ -269,7 +277,15 @@ describe('StaffService', function () {
|
|||||||
urlUtils,
|
urlUtils,
|
||||||
settingsHelpers,
|
settingsHelpers,
|
||||||
labs: {
|
labs: {
|
||||||
isSet: () => 'webmentions'
|
isSet: (flag) => {
|
||||||
|
if (flag === 'webmentions') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (flag === 'milestoneEmails') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -331,6 +347,21 @@ describe('StaffService', function () {
|
|||||||
sinon.match({subject: `💌 New mention from: Exmaple`})
|
sinon.match({subject: `💌 New mention from: Exmaple`})
|
||||||
).should.be.true();
|
).should.be.true();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles milestone created event', async function () {
|
||||||
|
await service.handleEvent(MilestoneCreatedEvent, {
|
||||||
|
data: {
|
||||||
|
milestone: {
|
||||||
|
type: 'arr',
|
||||||
|
value: '100',
|
||||||
|
currency: 'usd'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mailStub.called.should.be.false();
|
||||||
|
loggingInfoStub.calledOnce.should.be.true();
|
||||||
|
loggingInfoStub.calledWith('Will send email to owner@ghost.org for arr / 100 milestone.').should.be.true();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('notifyFreeMemberSignup', function () {
|
describe('notifyFreeMemberSignup', function () {
|
||||||
@ -635,5 +666,19 @@ describe('StaffService', function () {
|
|||||||
).should.be.true();
|
).should.be.true();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('notifyMilestoneReceived', function () {
|
||||||
|
it('prepares to send email when user setting available', async function () {
|
||||||
|
const milestone = {
|
||||||
|
type: 'members',
|
||||||
|
value: 25000
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyMilestoneReceived({milestone});
|
||||||
|
|
||||||
|
mailStub.called.should.be.false();
|
||||||
|
getEmailAlertUsersStub.calledWith('milestone-received').should.be.true();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user