mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
refs https://github.com/TryGhost/Team/issues/1470 Instead of counting the MRR by resolving all the deltas from the past until now, we should start with the current calculated MRR and resolve it until the first event. That would give a more accurate recent MRR (in exchange for a less accurate MRR for older data) and allows us to limit the amount of returned days in the future. - Includes MRR stats service that can fetch the current MRR per currency - The service can return a history of the MRR for every day and currency - New admin API endpoint /stats/mrr that returns the MRR history - Includes tests for these new service and endpoint
377 lines
11 KiB
JavaScript
377 lines
11 KiB
JavaScript
const MrrStatsService = require('../../../../../core/server/services/stats/lib/mrr-stats-service');
|
|
const moment = require('moment');
|
|
const sinon = require('sinon');
|
|
require('should');
|
|
|
|
describe('MrrStatsService', function () {
|
|
describe('getHistory', function () {
|
|
let mrrStatsService;
|
|
let fakeDeltas;
|
|
let fakeTotal;
|
|
|
|
/**
|
|
* @type {Object.<string, number>}
|
|
*/
|
|
let currentMrr = {};
|
|
/**
|
|
* @type {MrrStatsService.MrrDelta[]}
|
|
*/
|
|
let deltas = [];
|
|
|
|
const today = '2000-01-10';
|
|
const tomorrow = '2000-01-11';
|
|
const yesterday = '2000-01-09';
|
|
const dayBeforeYesterday = '2000-01-08';
|
|
const twoDaysBeforeYesterday = '2000-01-07';
|
|
const todayDate = moment(today).toDate();
|
|
const tomorrowDate = moment(tomorrow).toDate();
|
|
const yesterdayDate = moment(yesterday).toDate();
|
|
const dayBeforeYesterdayDate = moment(dayBeforeYesterday).toDate();
|
|
|
|
before(function () {
|
|
sinon.useFakeTimers(todayDate.getTime());
|
|
mrrStatsService = new MrrStatsService({db: null});
|
|
fakeTotal = sinon.stub(mrrStatsService, 'getCurrentMrr').callsFake(() => {
|
|
const arr = [];
|
|
const sortedCurrencies = Object.keys(currentMrr).sort();
|
|
for (const currency of sortedCurrencies) {
|
|
arr.push({
|
|
mrr: currentMrr[currency],
|
|
currency
|
|
});
|
|
}
|
|
|
|
// Make sure we sort by currency
|
|
return Promise.resolve(arr);
|
|
});
|
|
fakeDeltas = sinon.stub(mrrStatsService, 'fetchAllDeltas').callsFake(() => {
|
|
// Sort here alphabetically to mimic same ordering of fetchAllDeltas
|
|
// Not a real problem we sort in place
|
|
deltas.sort((a, b) => {
|
|
if (a.date === b.date) {
|
|
return a.currency < b.currency ? -1 : 1;
|
|
}
|
|
return a.date < b.date ? -1 : 1;
|
|
});
|
|
return Promise.resolve(deltas);
|
|
});
|
|
});
|
|
|
|
afterEach(function () {
|
|
fakeDeltas.resetHistory();
|
|
fakeTotal.resetHistory();
|
|
});
|
|
|
|
it('Always returns at least one value', async function () {
|
|
// No events
|
|
deltas = [];
|
|
currentMrr = {usd: 1, eur: 2};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(2);
|
|
|
|
// Note that currencies should always be sorted ascending, so EUR should be first.
|
|
results[0].should.eql({
|
|
date: today,
|
|
mrr: 2,
|
|
currency: 'eur'
|
|
});
|
|
results[1].should.eql({
|
|
date: today,
|
|
mrr: 1,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 2,
|
|
currency: 'eur'
|
|
},
|
|
{
|
|
mrr: 1,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Does not substract delta of first event', async function () {
|
|
deltas = [
|
|
{
|
|
date: todayDate,
|
|
delta: 5,
|
|
currency: 'usd'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 5};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(2);
|
|
results[0].should.eql({
|
|
date: yesterday,
|
|
mrr: 0,
|
|
currency: 'usd'
|
|
});
|
|
results[1].should.eql({
|
|
date: today,
|
|
mrr: 5,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 5,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Correctly calculates deltas', async function () {
|
|
deltas = [
|
|
{
|
|
date: yesterdayDate,
|
|
delta: 2,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: todayDate,
|
|
delta: 5,
|
|
currency: 'usd'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 7};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(3);
|
|
results[0].should.eql({
|
|
date: dayBeforeYesterday,
|
|
mrr: 0,
|
|
currency: 'usd'
|
|
});
|
|
results[1].should.eql({
|
|
date: yesterday,
|
|
mrr: 2,
|
|
currency: 'usd'
|
|
});
|
|
results[2].should.eql({
|
|
date: today,
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Correctly calculates deltas for multiple currencies', async function () {
|
|
deltas = [
|
|
{
|
|
date: yesterdayDate,
|
|
delta: 200,
|
|
currency: 'eur'
|
|
},
|
|
{
|
|
date: yesterdayDate,
|
|
delta: 2,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: todayDate,
|
|
delta: 800,
|
|
currency: 'eur'
|
|
},
|
|
{
|
|
date: todayDate,
|
|
delta: 5,
|
|
currency: 'usd'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 7, eur: 1200};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(6);
|
|
results[0].should.eql({
|
|
date: dayBeforeYesterday,
|
|
mrr: 200,
|
|
currency: 'eur'
|
|
});
|
|
results[1].should.eql({
|
|
date: dayBeforeYesterday,
|
|
mrr: 0,
|
|
currency: 'usd'
|
|
});
|
|
results[2].should.eql({
|
|
date: yesterday,
|
|
mrr: 400,
|
|
currency: 'eur'
|
|
});
|
|
results[3].should.eql({
|
|
date: yesterday,
|
|
mrr: 2,
|
|
currency: 'usd'
|
|
});
|
|
results[4].should.eql({
|
|
date: today,
|
|
mrr: 1200,
|
|
currency: 'eur'
|
|
});
|
|
results[5].should.eql({
|
|
date: today,
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 1200,
|
|
currency: 'eur'
|
|
},
|
|
{
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Ignores invalid currencies in deltas', async function () {
|
|
deltas = [
|
|
{
|
|
date: todayDate,
|
|
delta: 200,
|
|
currency: 'abc'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 7};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(1);
|
|
results[0].should.eql({
|
|
date: yesterday,
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Ignores events in the future', async function () {
|
|
deltas = [
|
|
{
|
|
date: yesterdayDate,
|
|
delta: 2,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: todayDate,
|
|
delta: 5,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: tomorrowDate,
|
|
delta: 10,
|
|
currency: 'usd'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 7};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(3);
|
|
results[0].should.eql({
|
|
date: dayBeforeYesterday,
|
|
mrr: 0,
|
|
currency: 'usd'
|
|
});
|
|
results[1].should.eql({
|
|
date: yesterday,
|
|
mrr: 2,
|
|
currency: 'usd'
|
|
});
|
|
results[2].should.eql({
|
|
date: today,
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
|
|
it('Correctly handles negative total MRR', async function () {
|
|
deltas = [
|
|
{
|
|
date: dayBeforeYesterdayDate,
|
|
delta: 2,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: yesterdayDate,
|
|
delta: -1000,
|
|
currency: 'usd'
|
|
},
|
|
{
|
|
date: todayDate,
|
|
delta: 1000,
|
|
currency: 'usd'
|
|
}
|
|
];
|
|
|
|
currentMrr = {usd: 7};
|
|
|
|
const {data: results, meta} = await mrrStatsService.getHistory();
|
|
results.length.should.eql(4);
|
|
results[0].should.eql({
|
|
date: twoDaysBeforeYesterday,
|
|
mrr: 5,
|
|
currency: 'usd'
|
|
});
|
|
results[1].should.eql({
|
|
date: dayBeforeYesterday,
|
|
// We are mainly testing that this should not be 1000!
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
results[2].should.eql({
|
|
date: yesterday,
|
|
// Should never be shown negative (in fact it is -993 here)
|
|
mrr: 0,
|
|
currency: 'usd'
|
|
});
|
|
results[3].should.eql({
|
|
date: today,
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
});
|
|
meta.totals.should.eql([
|
|
{
|
|
mrr: 7,
|
|
currency: 'usd'
|
|
}
|
|
]);
|
|
fakeDeltas.calledOnce.should.eql(true);
|
|
fakeTotal.calledOnce.should.eql(true);
|
|
});
|
|
});
|
|
});
|