Ghost/ghost/members-api/lib/repositories/event/index.js
Fabien O'Carroll 58c9c1c649 Cleaned newsletter subscription events from timeline
refs https://github.com/TryGhost/Team/issues/469

In order to reduce noise, we want to only display newsletter
subscription events which are not likely to be the result of a member
signup. The approach we've taken is to remove any newsletter
subscription (not unsubscription) event, if when sorted in chronological
order, it is to reside next to a signup event for the same member.

An improvement to this approach might be to add some kind of transaction
id  to events which would allow us to group together events which should
be considered to have happened simultaneously.
2021-03-05 12:59:05 +00:00

267 lines
8.1 KiB
JavaScript

module.exports = class EventRepository {
constructor({
MemberSubscribeEvent,
MemberPaymentEvent,
MemberStatusEvent,
MemberLoginEvent,
MemberPaidSubscriptionEvent,
logger
}) {
this._MemberSubscribeEvent = MemberSubscribeEvent;
this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent;
this._MemberPaymentEvent = MemberPaymentEvent;
this._MemberStatusEvent = MemberStatusEvent;
this._MemberLoginEvent = MemberLoginEvent;
this._logging = logger;
}
async registerPayment(data) {
await this._MemberPaymentEvent.add({
...data,
source: 'stripe'
});
}
async getNewsletterSubscriptionEvents(options = {}) {
options.withRelated = ['member'];
const {data: models, meta} = await this._MemberSubscribeEvent.findPage(options);
const data = models.map((data) => {
return {
type: 'newsletter_event',
data: data.toJSON(options)
};
});
return {
data,
meta
};
}
async getSubscriptionEvents(options = {}) {
options.withRelated = ['member'];
const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options);
const data = models.map((data) => {
return {
type: 'subscription_event',
data: data.toJSON(options)
};
});
return {
data,
meta
};
}
async getPaymentEvents(options = {}) {
options.withRelated = ['member'];
const {data: models, meta} = await this._MemberPaymentEvent.findPage(options);
const data = models.map((data) => {
return {
type: 'payment_event',
data: data.toJSON(options)
};
});
return {
data,
meta
};
}
async getLoginEvents(options = {}) {
options.withRelated = ['member'];
const {data: models, meta} = await this._MemberLoginEvent.findPage(options);
const data = models.map((data) => {
return {
type: 'login_event',
data: data.toJSON(options)
};
});
return {
data,
meta
};
}
async getSignupEvents(options = {}) {
options.withRelated = ['member'];
options.filter = 'from_status:null';
const {data: models, meta} = await this._MemberStatusEvent.findPage(options);
const data = models.map((data) => {
return {
type: 'signup_event',
data: data.toJSON(options)
};
});
return {
data,
meta
};
}
async getEventTimeline(options = {}) {
if (!options.limit) {
options.limit = 10;
}
options.order = 'created_at desc';
const allEventPages = await Promise.all([
this.getNewsletterSubscriptionEvents(options),
this.getSubscriptionEvents(options),
this.getLoginEvents(options),
this.getSignupEvents(options)
]);
const allEvents = allEventPages.reduce((allEvents, page) => allEvents.concat(page.data), []);
return allEvents.sort((a, b) => {
return new Date(b.data.created_at) - new Date(a.data.created_at);
}).reduce((memo, event, i) => {
if (event.type === 'newsletter_event' && event.data.subscribed) {
const previousEvent = allEvents[i - 1];
const nextEvent = allEvents[i + 1];
const currentMember = event.data.member_id;
if (previousEvent && previousEvent.type === 'signup_event') {
const previousMember = previousEvent.data.member_id;
if (currentMember === previousMember) {
return memo;
}
}
if (nextEvent && nextEvent.type === 'signup_event') {
const nextMember = nextEvent.data.member_id;
if (currentMember === nextMember) {
return memo;
}
}
}
return memo.concat(event);
}, []).slice(0, options.limit);
}
async getSubscriptions() {
const results = await this._MemberSubscribeEvent.findAll({
aggregateSubscriptionDeltas: true
});
const resultsJSON = results.toJSON();
const cumulativeResults = resultsJSON.reduce((cumulativeResults, result, index) => {
if (index === 0) {
return [{
date: result.date,
subscribed: result.subscribed_delta
}];
}
return cumulativeResults.concat([{
date: result.date,
subscribed: result.subscribed_delta + cumulativeResults[index - 1].subscribed
}]);
}, []);
return cumulativeResults;
}
async getMRR() {
const results = await this._MemberPaidSubscriptionEvent.findAll({
aggregateMRRDeltas: true
});
const resultsJSON = results.toJSON();
const cumulativeResults = resultsJSON.reduce((cumulativeResults, result) => {
if (!cumulativeResults[result.currency]) {
return {
...cumulativeResults,
[result.currency]: [{
date: result.date,
mrr: result.mrr_delta,
currency: result.currency
}]
};
}
return {
...cumulativeResults,
[result.currency]: cumulativeResults[result.currency].concat([{
date: result.date,
mrr: result.mrr_delta + cumulativeResults[result.currency].slice(-1)[0].mrr,
currency: result.currency
}])
};
}, {});
return cumulativeResults;
}
async getVolume() {
const results = await this._MemberPaymentEvent.findAll({
aggregatePaymentVolume: true
});
const resultsJSON = results.toJSON();
const cumulativeResults = resultsJSON.reduce((cumulativeResults, result) => {
if (!cumulativeResults[result.currency]) {
return {
...cumulativeResults,
[result.currency]: [{
date: result.date,
volume: result.volume_delta,
currency: result.currency
}]
};
}
return {
...cumulativeResults,
[result.currency]: cumulativeResults[result.currency].concat([{
date: result.date,
volume: result.volume_delta + cumulativeResults[result.currency].slice(-1)[0].volume,
currency: result.currency
}])
};
}, {});
return cumulativeResults;
}
async getStatuses() {
const results = await this._MemberStatusEvent.findAll({
aggregateStatusCounts: true
});
const resultsJSON = results.toJSON();
const cumulativeResults = resultsJSON.reduce((cumulativeResults, result, index) => {
if (index === 0) {
return [{
date: result.date,
paid: result.paid_delta,
comped: result.comped_delta,
free: result.free_delta
}];
}
return cumulativeResults.concat([{
date: result.date,
paid: result.paid_delta + cumulativeResults[index - 1].paid,
comped: result.comped_delta + cumulativeResults[index - 1].comped,
free: result.free_delta + cumulativeResults[index - 1].free
}]);
}, []);
return cumulativeResults;
}
};