0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00
ghost/test/unit/server/services/stats/mrr-stats-service.test.js
Simon Backx 132726fe20
Added MRR stats service and endpoint (#14427)
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
2022-04-08 09:18:04 +02:00

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