0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added milestone email service behind alpha flag (#16241)

refs
https://www.notion.so/ghost/Marketing-Milestone-email-campaigns-1d2c9dee3cfa4029863edb16092ad5c4

Added milestone email service behind a flag. The service will currently
run on boot and is meant to be scheduled soon, which should happen in
the next step. For now it's protected behind the alpha flag.
This commit is contained in:
Aileen Booker 2023-02-13 16:29:01 +02:00 committed by GitHub
parent 48f9485f46
commit 6f0d1b0ff9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 547 additions and 109 deletions

View file

@ -370,6 +370,9 @@ async function initBackgroundServices({config}) {
const updateCheck = require('./server/update-check');
updateCheck.scheduleRecurringJobs();
const milestoneEmails = require('./server/services/milestone-emails');
milestoneEmails.initAndRun();
debug('End: initBackgroundServices');
}

View file

@ -0,0 +1,58 @@
const MIN_DAYS_SINCE_IMPORTED = 7;
module.exports = class MilestoneQueries {
#db;
constructor(deps) {
this.#db = deps.db;
}
/**
* @returns {Promise<number>}
*/
async getMembersCount() {
const [membersCount] = await this.#db.knex('members').count('id as count');
return membersCount?.count || 0;
}
/**
* @returns {Promise<Array>}
*/
async getARR() {
const currentARR = await this.#db.knex('members_paid_subscription_events as stripe')
.select(this.#db.knex.raw('ROUND(SUM(stripe.mrr_delta) * 12) / 100 AS arr, stripe.currency as currency'))
.groupBy('stripe.currency');
return currentARR;
}
/**
* @returns {Promise<boolean>}
*/
async hasImportedMembersInPeriod() {
const [hasImportedMembers] = await this.#db.knex('members_subscribe_events')
.count('id as count')
.where('source', '=', 'import')
.where('created_at', '>=', MIN_DAYS_SINCE_IMPORTED);
return hasImportedMembers?.count > 0;
}
/**
* @returns {Promise<string>}
*/
async getDefaultCurrency() {
const currentARR = await this.getARR();
// Set the default currency as the one with the highest value
if (currentARR.length > 1) {
const highestValues = currentARR.sort((a, b) => b.arr - a.arr);
return highestValues?.[0]?.currency;
} else if (currentARR?.[0]?.currency) {
return currentARR[0].currency;
} else {
return 'usd';
}
}
};

View file

@ -0,0 +1 @@
module.exports = require('./service');

View file

@ -0,0 +1,58 @@
// Stubbing stripe in test was causing issues. Moved it
// into this function to be able to rewire and stub the
// expected return value.
const getStripeLiveEnabled = () => {
const stripeService = require('../stripe');
// This seems to be the only true way to check if Stripe is configured in live mode
// settingsCache only cares if Stripe is enabled
return stripeService.api.configured && stripeService.api.mode === 'live';
};
/**
*
* @returns {Promise<any>}
*/
module.exports = {
async initAndRun() {
const labs = require('../../../shared/labs');
if (labs.isSet('milestoneEmails')) {
const db = require('../../data/db');
const MilestoneQueries = require('./MilestoneQueries');
const {
MilestonesEmailService,
InMemoryMilestoneRepository
} = require('@tryghost/milestone-emails');
const config = require('../../../shared/config');
const milestonesConfig = config.get('milestones');
const {GhostMailer} = require('../mail');
const mailer = new GhostMailer();
const repository = new InMemoryMilestoneRepository();
const queries = new MilestoneQueries({db});
const milestonesEmailService = new MilestonesEmailService({
mailer,
repository,
milestonesConfig, // avoid using getters and pass as JSON
queries
});
let arrResult;
// @TODO: schedule recurring jobs instead
const membersResult = await milestonesEmailService.checkMilestones('members');
const stripeLiveEnabled = getStripeLiveEnabled();
if (stripeLiveEnabled) {
arrResult = await milestonesEmailService.checkMilestones('arr');
}
return {
members: membersResult,
arr: arrResult
};
}
}
};

View file

@ -113,6 +113,7 @@
"@tryghost/members-stripe-service": "0.0.0",
"@tryghost/metrics": "1.0.20",
"@tryghost/minifier": "0.0.0",
"@tryghost/milestone-emails": "0.0.0",
"@tryghost/mw-api-version-mismatch": "0.0.0",
"@tryghost/mw-cache-control": "0.0.0",
"@tryghost/mw-error-handler": "0.0.0",

View file

@ -0,0 +1,228 @@
const {agentProvider, fixtureManager, mockManager, configUtils} = require('../../utils/e2e-framework');
const assert = require('assert');
const nock = require('nock');
const sinon = require('sinon');
const models = require('../../../core/server/models');
const moment = require('moment');
const milestoneEmailsService = require('../../../core/server/services/milestone-emails/service');
let agent;
let counter = 0;
let membersCounter = 0;
async function createMemberWithSubscription(interval, amount, currency, date) {
counter += 1;
membersCounter += 1;
const fakePrice = {
id: 'price_' + counter,
product: '',
active: true,
nickname: 'Paid',
unit_amount: amount,
currency,
type: 'recurring',
recurring: {
interval
}
};
const fakeSubscription = {
id: 'sub_' + counter,
customer: 'cus_' + counter,
status: 'active',
cancel_at_period_end: false,
metadata: {},
current_period_end: Date.now() / 1000 + 1000,
start_date: moment(date).unix(),
plan: fakePrice,
items: {
data: [{
price: fakePrice
}]
}
};
const fakeCustomer = {
id: 'cus_' + counter,
name: 'Test Member',
email: 'create-member-subscription-' + counter + '@email.com',
subscriptions: {
type: 'list',
data: [fakeSubscription]
}
};
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri, body) => {
const [match, resource, id] = uri.match(/\/?v1\/(\w+)\/?(\w+)/) || [null];
if (!match) {
return [500];
}
if (resource === 'customers') {
return [200, fakeCustomer];
}
if (resource === 'subscriptions') {
return [200, fakeSubscription];
}
});
const initialMember = {
name: fakeCustomer.name,
email: fakeCustomer.email,
subscribed: true,
stripe_customer_id: fakeCustomer.id
};
await agent
.post(`/members/`)
.body({members: [initialMember]})
.expectStatus(201);
nock.cleanAll();
}
async function createFreeMembers(amount, amountImported = 0) {
const members = [];
const newsletters = await agent.get(`/newsletters/`);
const newsletter = newsletters.body?.newsletters?.[0];
for (let index = 0; index < amount; index++) {
let membersAddRequest;
membersCounter += 1;
const member = {
name: 'Test Member',
email: 'free-member-' + membersCounter + '@email.com',
status: 'free',
uuid: `f6f91461-d7d8-4a3f-aa5d-8e582c40b99${membersCounter}`
};
if (amountImported > 0) {
member.subscribed = true;
member.newsletters = [newsletter];
const createMemberEvent = await agent
.post(`/members/`)
.body({members: [member]})
.expectStatus(201);
const id = createMemberEvent.body.members[0].id;
// Manually add the members_subscribe_event so we can test imported members
const editedEvent = await models.MemberSubscribeEvent.add({
newsletter_id: newsletter.id,
member_id: id,
subscribed: true,
source: index < amountImported ? 'import' : 'member'
});
membersAddRequest = Promise.all([createMemberEvent, editedEvent]);
} else {
membersAddRequest = await agent
.post(`/members/`)
.body({members: [member]})
.expectStatus(201);
}
members.push(membersAddRequest);
}
await Promise.all(members);
}
describe('Milestone Emails Service', function () {
// let stripeModeStub;
const milestonesConfig = {
arr: [{currency: 'usd', values: [100]}],
members: [10, 100]
};
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('newsletters');
await agent.loginAsOwner();
});
beforeEach(async function () {
sinon.createSandbox();
// TODO: stub out stripe mode
// stripeModeStub = sinon.stub().returns(true);
// milestoneEmailsService.__set__('getStripeLiveEnabled', stripeModeStub);
configUtils.set('milestones', milestonesConfig);
mockManager.mockLabsEnabled('milestoneEmails');
});
afterEach(async function () {
await configUtils.restore();
mockManager.restore();
sinon.restore();
});
it('Runs ARR and Members milestone jobs', async function () {
// No ARR and no members
const firstRun = await milestoneEmailsService.initAndRun();
assert(firstRun.members === undefined);
// assert(firstRun.arr === undefined);
await createFreeMembers(7);
await createMemberWithSubscription('year', 5000, 'usd', '2000-01-10');
await createMemberWithSubscription('month', 100, 'usd', '2000-01-10');
const secondRun = await milestoneEmailsService.initAndRun();
assert(secondRun.members === undefined);
// assert(secondRun.arr === undefined);
// Reached the first milestone for members
await createFreeMembers(1);
const thirdRun = await milestoneEmailsService.initAndRun();
assert(thirdRun.members.value === 10);
assert(thirdRun.members.emailSentAt !== undefined);
// assert(thirdRun.arr === undefined);
// Reached the first milestone for ARR
await createMemberWithSubscription('month', 500, 'usd', '2000-01-10');
await createMemberWithSubscription('month', 500, 'eur', '2000-01-10');
const fourthRun = await milestoneEmailsService.initAndRun();
// This will be false once we hook up to the DB
assert(fourthRun.members.value === 10);
assert(fourthRun.members.emailSentAt !== undefined);
// assert(fourthRun.arr.value === 100);
// assert(fourthRun.arr.emailSentAt !== undefined);
});
it('Does not send emails for milestones when imported members present', async function () {
await createFreeMembers(10, 1);
await createMemberWithSubscription('month', 1000, 'usd', '2023-01-10');
const result = await milestoneEmailsService.initAndRun();
assert(result.members.value === 10);
assert(result.members.emailSentAt === null);
// assert(result.arr.value === 100);
// assert(result.arr.emailSentAt === null);
});
it('Does not run when milestoneEmails labs flag is not set', async function () {
mockManager.mockLabsDisabled('milestoneEmails');
const result = await milestoneEmailsService.initAndRun();
assert(result === undefined);
});
// it('Does not run ARR milestones when Stripe is not live enabled', async function () {
// stripeModeStub = sinon.stub().returns(false);
// milestoneEmailsService.__set__('getStripeLiveEnabled', stripeModeStub);
// await createFreeMembers(10);
// const result = await milestoneEmailsService.initAndRun();
// assert(result.members.value === 10);
// assert(result.members.emailSentAt !== undefined);
// assert(result.arr === undefined);
// });
});

View file

@ -0,0 +1,37 @@
const db = require('../../../../../core/server/data/db');
const assert = require('assert');
const sinon = require('sinon');
describe('MilestoneQueries', function () {
let milestoneQueries;
let queryMock;
let knexMock;
before(function () {
queryMock = {
groupBy: sinon.stub(),
select: sinon.stub(),
raw: sinon.stub(),
count: sinon.stub(),
where: sinon.stub()
};
knexMock = sinon.stub().returns(queryMock);
sinon.stub(db, 'knex').get(function () {
return knexMock;
});
});
describe('Milestone Emails Service', function () {
it('Provides expected public API', async function () {
const MilestoneQueries = require('../../../../../core/server/services/milestone-emails/MilestoneQueries');
milestoneQueries = new MilestoneQueries({db: knexMock});
assert.ok(milestoneQueries.getMembersCount);
assert.ok(milestoneQueries.getARR);
assert.ok(milestoneQueries.hasImportedMembersInPeriod);
assert.ok(milestoneQueries.getDefaultCurrency);
});
});
});

View file

@ -0,0 +1,13 @@
const assert = require('assert');
describe('Milestone Emails Service', function () {
let milestoneEmails;
describe('Milestone Emails Service', function () {
it('Provides expected public API', async function () {
milestoneEmails = require('../../../../../core/server/services/milestone-emails');
assert.ok(milestoneEmails.initAndRun);
});
});
});

View file

@ -1,6 +1,6 @@
/**
* @typedef {import('./Milestone')} Milestone
* @typedef {import('./MilestonesAPI').IMilestoneRepository} IMilestoneRepository
* @typedef {import('./MilestonesEmailService').IMilestoneRepository} IMilestoneRepository
*/
/**
@ -32,7 +32,7 @@ module.exports = class InMemoryMilestoneRepository {
/**
* @param {'arr'|'members'} type
* @param {string|null} currency
* @param {string} [currency]
*
* @returns {Promise<Milestone>}
*/
@ -68,7 +68,7 @@ module.exports = class InMemoryMilestoneRepository {
/**
* @param {number} value
* @param {string} currency
* @param {string} [currency]
*
* @returns {Promise<Milestone>}
*/

View file

@ -151,9 +151,9 @@ function validateValue(value) {
/**
*
* @param {'arr'|'members'} type
* @param {unknown} type
*
* @returns {string}
* @returns {'arr'|'members'}
*/
function validateType(type) {
if (type === 'arr') {
@ -201,7 +201,7 @@ function validateName(name, value, type, currency) {
/**
*
* @param {Object} data
* @param {object} data
* @param {Date|null} data.emailSentAt
*
* @returns {Date|null}

View file

@ -1,66 +1,66 @@
const Milestone = require('./Milestone');
/**
* @template Model
* @typedef {object} Mention<Model>
* @prop {Model[]} data
*/
/**
* @typedef {object} IMilestoneRepository
* @prop {(milestone: Milestone) => Promise<void>} save
* @prop {(arr: number) => Promise<Milestone>} getByARR
* @prop {(arr: number, [currency]: string|null) => Promise<Milestone>} getByARR
* @prop {(count: number) => Promise<Milestone>} getByCount
* @prop {(type: 'arr'|'members') => Promise<Milestone>} getLatestByType
* @prop {(type: 'arr'|'members', [currency]: string|null) => Promise<Milestone>} getLatestByType
* @prop {() => Promise<Milestone>} getLastEmailSent
*/
/**
* @template Model
* @typedef {import('./MilestonesAPI')} <Model>
* @typedef {object} IQueries
* @prop {() => Promise<number>} getMembersCount
* @prop {() => Promise<object>} getARR
* @prop {() => Promise<boolean>} hasImportedMembersInPeriod
* @prop {() => Promise<string>} getDefaultCurrency
*/
/**
* @typedef {Object} IQueries
* @prop {() => Promise<number>} getMembersCount
* @prop {() => Promise<Object>} getARR
* @prop {() => Promise<boolean>} hasImportedMembersInPeriod
* @typedef {object} ghostMailer
* @property {Function} send
*/
/**
* @typedef {object} milestonesConfig
* @prop {Array<object>} milestonesConfig.arr
* @prop {string} milestonesConfig.arr.currency
* @prop {number[]} milestonesConfig.arr.values
* @prop {number[]} milestonesConfig.members
*/
module.exports = class MilestonesEmailService {
/** @type {IMilestoneRepository} */
#repository;
/** @type {Function} */
/**
* @type {ghostMailer} */
#mailer;
/** @type {Object} */
#config;
/**
* @type {milestonesConfig} */
#milestonesConfig;
/** @type {IQueries} */
#queries;
/** @type {string} */
#defaultCurrency;
/**
* @param {object} deps
* @param {Function} deps.mailer
* @param {MilestonesAPI} deps.api
* @param {Object} deps.config
* @param {ghostMailer} deps.mailer
* @param {IMilestoneRepository} deps.repository
* @param {milestonesConfig} deps.milestonesConfig
* @param {IQueries} deps.queries
* @param {string} deps.defaultCurrency
*/
constructor(deps) {
this.#mailer = deps.mailer;
this.#config = deps.config;
this.#milestonesConfig = deps.milestonesConfig;
this.#queries = deps.queries;
this.#defaultCurrency = deps.defaultCurrency;
this.#repository = deps.repository;
}
/**
* @param {string|null} currency
* @param {string} [currency]
*
* @returns {Promise<Milestone>}
*/
@ -72,7 +72,14 @@ module.exports = class MilestonesEmailService {
* @returns {Promise<Milestone>}
*/
async #getLatestMembersCountMilestone() {
return this.#repository.getLatestByType('members');
return this.#repository.getLatestByType('members', null);
}
/**
* @returns {Promise<string>}
*/
async #getDefaultCurrency() {
return await this.#queries.getDefaultCurrency();
}
/**
@ -88,7 +95,7 @@ module.exports = class MilestonesEmailService {
let existingMilestone = null;
if (milestone.type === 'arr') {
existingMilestone = await this.#repository.getByARR(milestone.value, milestone?.currency) || false;
existingMilestone = await this.#repository.getByARR(milestone.value, milestone.currency) || false;
} else if (milestone.type === 'members') {
existingMilestone = await this.#repository.getByCount(milestone.value) || false;
}
@ -115,10 +122,10 @@ module.exports = class MilestonesEmailService {
/**
*
* @param {Array} goalValues
* @param {number[]} goalValues
* @param {number} current
*
* @returns {Array}
* @returns {number}
*/
#getMatchedMilestone(goalValues, current) {
// return highest suitable milestone
@ -128,22 +135,19 @@ module.exports = class MilestonesEmailService {
/**
*
* @param {Object} milestone
* @param {object} milestone
* @param {number} milestone.value
* @param {'arr'|'members'} milestone.type
* @param {boolean} hasMembersImported
* @param {string|null} [milestone.currency]
* @param {Date|null} [milestone.emailSentAt]
*
* @returns {Promise<Milestone>}
*/
async #saveMileStoneAndSendEmail(milestone) {
if (milestone.type === 'arr') {
milestone.currency = this.#defaultCurrency;
}
const shouldSendEmail = await this.#shouldSendEmail();
if (shouldSendEmail) {
// TODO: hook up GhostMailer or use StaffService and trigger event to send email
// TODO: hook up Ghostmailer or use StaffService and trigger event to send email
// await this.#mailer.send({
// subject: 'Test',
// html: '<div>Milestone achieved</div>',
@ -187,29 +191,33 @@ module.exports = class MilestonesEmailService {
async #runARRQueries() {
// Fetch the current data from queries
const currentARR = await this.#queries.getARR();
const defaultCurrency = await this.#getDefaultCurrency();
// Check the definitions in the config
const arrMilestoneSettings = this.#config.milestones.arr;
// Check the definitions in the milestonesConfig
const arrMilestoneSettings = this.#milestonesConfig.arr;
const supportedCurrencies = arrMilestoneSettings.map(setting => setting.currency);
// First check the currency matches
if (currentARR.length) {
let milestone;
const currentARRForCurrency = currentARR.filter(arr => arr.currency === this.#defaultCurrency)[0];
const milestonesForCurrency = arrMilestoneSettings.filter(milestoneSetting => milestoneSetting.currency === this.#defaultCurrency)[0];
const currentARRForCurrency = currentARR.filter(arr => arr.currency === defaultCurrency && supportedCurrencies.includes(defaultCurrency))[0];
const milestonesForCurrency = arrMilestoneSettings.filter(milestoneSetting => milestoneSetting.currency === defaultCurrency)[0];
if (milestonesForCurrency && currentARRForCurrency) {
// get the closest milestone we're over now
milestone = this.#getMatchedMilestone(milestonesForCurrency.values, currentARRForCurrency.arr);
// Fetch the latest milestone for this currency
const latestMilestone = await this.#getLatestArrMilestone(this.#defaultCurrency);
const latestMilestone = await this.#getLatestArrMilestone(defaultCurrency);
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'arr', currency: this.#defaultCurrency});
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'arr', currency: defaultCurrency});
if ((!milestoneExists && !latestMilestone || milestone > latestMilestone.value)) {
return await this.#saveMileStoneAndSendEmail({value: milestone, type: 'arr'});
if (milestone && milestone > 0) {
if (!milestoneExists && (!latestMilestone || milestone > latestMilestone.value)) {
return await this.#saveMileStoneAndSendEmail({value: milestone, type: 'arr', currency: defaultCurrency});
}
}
}
}
@ -222,20 +230,22 @@ module.exports = class MilestonesEmailService {
// Fetch the current data
const membersCount = await this.#queries.getMembersCount();
// Check the definitions in the config
const membersMilestones = this.#config.milestones.members;
// Check the definitions in the milestonesConfig
const membersMilestones = this.#milestonesConfig.members;
// get the closest milestone we're over now
const milestone = this.#getMatchedMilestone(membersMilestones, membersCount);
let milestone = this.#getMatchedMilestone(membersMilestones, membersCount);
// Fetch the latest achieved Members milestones
const latestMembersMilestone = await this.#getLatestMembersCountMilestone();
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'members'});
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'members', currency: null});
if ((!milestoneExists && !latestMembersMilestone || milestone > latestMembersMilestone.value)) {
return await this.#saveMileStoneAndSendEmail({value: milestone, type: 'members'});
if (milestone && milestone > 0) {
if (!milestoneExists && (!latestMembersMilestone || milestone > latestMembersMilestone.value)) {
return await this.#saveMileStoneAndSendEmail({value: milestone, type: 'members'});
}
}
}

View file

@ -8,25 +8,27 @@ const Milestone = require('../lib/Milestone');
describe('MilestonesEmailService', function () {
let repository;
const milestoneConfig = {
milestones:
{
arr: [
{
currency: 'usd',
values: [1000, 10000, 50000, 100000, 250000, 500000, 1000000]
},
{
currency: 'gbp',
values: [500, 1000, 5000, 100000, 250000, 500000, 1000000]
},
{
currency: 'idr',
values: [1000, 10000, 50000, 100000, 250000, 500000, 1000000]
}
],
members: [100, 1000, 10000, 50000, 100000, 250000, 500000, 1000000]
}
const milestonesConfig = {
arr: [
{
currency: 'usd',
values: [1000, 10000, 50000, 100000, 250000, 500000, 1000000]
},
{
currency: 'gbp',
values: [500, 1000, 5000, 100000, 250000, 500000, 1000000]
},
{
currency: 'idr',
values: [1000, 10000, 50000, 100000, 250000, 500000, 1000000]
},
{
currency: 'eur',
values: [1000, 10000, 50000, 100000, 250000, 500000, 1000000]
}
],
members: [100, 1000, 10000, 50000, 100000, 250000, 500000, 1000000]
};
describe('ARR Milestones', function () {
@ -39,16 +41,18 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'usd', arr: 1298}, {currency: 'gbp', arr: 2600}];
return [{currency: 'usd', arr: 1298}, {currency: 'nzd', arr: 600}];
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
},
defaultCurrency: 'usd'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
@ -64,7 +68,7 @@ describe('MilestonesEmailService', function () {
const milestoneOne = await Milestone.create({
type: 'arr',
value: 1000,
value: 100,
createdAt: '2023-01-01T00:00:00Z',
emailSentAt: '2023-01-01T00:00:00Z'
});
@ -79,7 +83,7 @@ describe('MilestonesEmailService', function () {
const milestoneThree = await Milestone.create({
type: 'arr',
value: 1000,
currency: 'aud',
currency: 'eur',
createdAt: '2023-01-15T00:00:00Z',
emailSentAt: '2023-01-15T00:00:00Z'
});
@ -94,24 +98,27 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'usd', arr: 50005}];
// Same ARR values for both supported currencies
return [{currency: 'usd', arr: 10001}, {currency: 'eur', arr: 10001}];
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
},
defaultCurrency: 'usd'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
assert(arrResult.type === 'arr');
assert(arrResult.currency === 'usd');
assert(arrResult.value === 50000);
assert(arrResult.value === 10000);
assert(arrResult.emailSentAt !== null);
assert(arrResult.name === 'arr-50000-usd');
assert(arrResult.name === 'arr-10000-usd');
});
it('Does not add ARR milestone for out of scope currency', async function () {
@ -123,16 +130,18 @@ describe('MilestonesEmailService', function () {
mailer: {
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'nzd', arr: 1005}];
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'nzd';
}
},
defaultCurrency: 'nzd'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
@ -156,16 +165,18 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'gbp', arr: 5005}];
return [{currency: 'gbp', arr: 5005}, {currency: 'usd', arr: 100}];
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'gbp';
}
},
defaultCurrency: 'gbp'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
@ -181,16 +192,18 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'usd', arr: 100000}, {currency: 'idr', arr: 2600}];
},
async hasImportedMembersInPeriod() {
return true;
},
async getDefaultCurrency() {
return 'usd';
}
},
defaultCurrency: 'usd'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
@ -221,16 +234,18 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getARR() {
return [{currency: 'idr', arr: 10000}];
},
async hasImportedMembersInPeriod() {
return true;
},
async getDefaultCurrency() {
return 'idr';
}
},
defaultCurrency: 'idr'
}
});
const arrResult = await milestoneEmailService.checkMilestones('arr');
@ -251,13 +266,16 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getMembersCount() {
return 110;
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
}
});
@ -302,16 +320,18 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getMembersCount() {
return 50005;
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
},
defaultCurrency: 'usd'
}
});
const membersResult = await milestoneEmailService.checkMilestones('members');
@ -338,13 +358,16 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getMembersCount() {
return 50555;
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
}
});
@ -369,13 +392,16 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getMembersCount() {
return 1001;
},
async hasImportedMembersInPeriod() {
return true;
},
async getDefaultCurrency() {
return 'usd';
}
}
});
@ -406,13 +432,16 @@ describe('MilestonesEmailService', function () {
// TODO: make this a stub
send: async () => {}
},
config: milestoneConfig,
milestonesConfig,
queries: {
async getMembersCount() {
return 50010;
},
async hasImportedMembersInPeriod() {
return false;
},
async getDefaultCurrency() {
return 'usd';
}
}
});