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/members-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

304 lines
9.9 KiB
JavaScript

const MembersStatsService = require('../../../../../core/server/services/stats/lib/members-stats-service');
const moment = require('moment');
const sinon = require('sinon');
require('should');
describe('MembersStatsService', function () {
describe('getCountHistory', function () {
let membersStatsService;
let fakeStatuses;
let fakeTotal;
/**
* @type {MembersStatsService.TotalMembersByStatus}
*/
const currentCounts = {paid: 0, free: 0, comped: 0};
/**
* @type {MembersStatsService.MemberStatusDelta[]}
*/
let events = [];
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());
membersStatsService = new MembersStatsService({db: null});
fakeTotal = sinon.stub(membersStatsService, 'getCount').resolves(currentCounts);
fakeStatuses = sinon.stub(membersStatsService, 'fetchAllStatusDeltas').callsFake(() => {
// Sort here ascending to mimic same ordering
events.sort((a, b) => {
return a.date < b.date ? -1 : 1;
});
return Promise.resolve(events);
});
});
afterEach(function () {
fakeStatuses.resetHistory();
fakeTotal.resetHistory();
});
it('Always returns at least one value', async function () {
// No status events
events = [];
currentCounts.paid = 1;
currentCounts.free = 2;
currentCounts.comped = 3;
const {data: results, meta} = await membersStatsService.getCountHistory();
results.length.should.eql(1);
results[0].should.eql({
date: today,
paid: 1,
free: 2,
comped: 3,
paid_subscribed: 0,
paid_canceled: 0
});
meta.totals.should.eql(currentCounts);
fakeStatuses.calledOnce.should.eql(true);
fakeTotal.calledOnce.should.eql(true);
});
it('Passes paid_subscribers and paid_canceled', async function () {
// Update faked status events
events = [
{
date: todayDate,
paid_subscribed: 4,
paid_canceled: 3,
free_delta: 2,
comped_delta: 3
}
];
// Update current faked counts
currentCounts.paid = 1;
currentCounts.free = 2;
currentCounts.comped = 3;
const {data: results, meta} = await membersStatsService.getCountHistory();
results.should.eql([
{
date: yesterday,
paid: 0,
free: 0,
comped: 0,
paid_subscribed: 0,
paid_canceled: 0
},
{
date: today,
paid: 1,
free: 2,
comped: 3,
paid_subscribed: 4,
paid_canceled: 3
}
]);
meta.totals.should.eql(currentCounts);
fakeStatuses.calledOnce.should.eql(true);
fakeTotal.calledOnce.should.eql(true);
});
it('Correctly resolves deltas', async function () {
// Update faked status events
events = [
{
date: yesterdayDate,
paid_subscribed: 2,
paid_canceled: 1,
free_delta: 0,
comped_delta: 0
},
{
date: todayDate,
paid_subscribed: 4,
paid_canceled: 3,
free_delta: 2,
comped_delta: 3
}
];
// Update current faked counts
currentCounts.paid = 2;
currentCounts.free = 3;
currentCounts.comped = 4;
const {data: results, meta} = await membersStatsService.getCountHistory();
results.should.eql([
{
date: dayBeforeYesterday,
paid: 0,
free: 1,
comped: 1,
paid_subscribed: 0,
paid_canceled: 0
},
{
date: yesterday,
paid: 1,
free: 1,
comped: 1,
paid_subscribed: 2,
paid_canceled: 1
},
{
date: today,
paid: 2,
free: 3,
comped: 4,
paid_subscribed: 4,
paid_canceled: 3
}
]);
meta.totals.should.eql(currentCounts);
fakeStatuses.calledOnce.should.eql(true);
fakeTotal.calledOnce.should.eql(true);
});
it('Correctly handles negative numbers', async function () {
// Update faked status events
events = [
{
date: dayBeforeYesterdayDate,
paid_subscribed: 2,
paid_canceled: 1,
free_delta: 2,
comped_delta: 10
},
{
date: yesterdayDate,
paid_subscribed: 2,
paid_canceled: 1,
free_delta: -100,
comped_delta: 0
},
{
date: todayDate,
paid_subscribed: 4,
paid_canceled: 3,
free_delta: 100,
comped_delta: 3
}
];
// Update current faked counts
currentCounts.paid = 2;
currentCounts.free = 3;
currentCounts.comped = 4;
const {data: results, meta} = await membersStatsService.getCountHistory();
results.should.eql([
{
date: twoDaysBeforeYesterday,
paid: 0,
free: 1,
comped: 0,
paid_subscribed: 0,
paid_canceled: 0
},
{
date: dayBeforeYesterday,
paid: 0,
// note that this shouldn't be 100 (which is also what we test here):
free: 3,
comped: 1,
paid_subscribed: 2,
paid_canceled: 1
},
{
date: yesterday,
paid: 1,
// never return negative numbers, this is in fact -997:
free: 0,
comped: 1,
paid_subscribed: 2,
paid_canceled: 1
},
{
date: today,
paid: 2,
free: 3,
comped: 4,
paid_subscribed: 4,
paid_canceled: 3
}
]);
meta.totals.should.eql(currentCounts);
fakeStatuses.calledOnce.should.eql(true);
fakeTotal.calledOnce.should.eql(true);
});
it('Ignores events in the future', async function () {
// Update faked status events
events = [
{
date: yesterdayDate,
paid_subscribed: 1,
paid_canceled: 0,
free_delta: 1,
comped_delta: 0
},
{
date: todayDate,
paid_subscribed: 4,
paid_canceled: 3,
free_delta: 2,
comped_delta: 3
},
{
date: tomorrowDate,
paid_subscribed: 10,
paid_canceled: 5,
free_delta: 8,
comped_delta: 9
}
];
// Update current faked counts
currentCounts.paid = 1;
currentCounts.free = 2;
currentCounts.comped = 3;
const {data: results, meta} = await membersStatsService.getCountHistory();
results.should.eql([
{
date: dayBeforeYesterday,
paid: 0,
free: 0,
comped: 0,
paid_subscribed: 0,
paid_canceled: 0
},
{
date: yesterday,
paid: 0,
free: 0,
comped: 0,
paid_subscribed: 1,
paid_canceled: 0
},
{
date: today,
paid: 1,
free: 2,
comped: 3,
paid_subscribed: 4,
paid_canceled: 3
}
]);
meta.totals.should.eql(currentCounts);
fakeStatuses.calledOnce.should.eql(true);
fakeTotal.calledOnce.should.eql(true);
});
});
});