51e3b0ac11
refs https://github.com/TryGhost/Team/issues/1673 - We don't want to show updated subscriptions in the dashboard - We need to send add fields to accomplish this - Includes updated tests
179 lines
6.3 KiB
JavaScript
179 lines
6.3 KiB
JavaScript
const moment = require('moment');
|
|
|
|
class SubscriptionStatsService {
|
|
/**
|
|
* @param {object} deps
|
|
* @param {import('knex').Knex} deps.knex*/
|
|
constructor({knex}) {
|
|
this.knex = knex;
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<{data: SubscriptionHistoryEntry[]}>}
|
|
**/
|
|
async getSubscriptionHistory() {
|
|
const subscriptionDeltaEntries = await this.fetchAllSubscriptionDeltas();
|
|
const counts = await this.fetchSubscriptionCounts();
|
|
|
|
/** @type {Object.<string, Object.<string, number>>} */
|
|
const countData = {};
|
|
counts.forEach((count) => {
|
|
if (!countData[count.tier]) {
|
|
countData[count.tier] = {};
|
|
}
|
|
countData[count.tier][count.cadence] = count.count;
|
|
});
|
|
|
|
/** @type {SubscriptionHistoryEntry[]} */
|
|
let subscriptionHistoryEntries = [];
|
|
|
|
/** @type {string[]} */
|
|
let cadences = [];
|
|
/** @type {string[]} */
|
|
let tiers = [];
|
|
|
|
for (let index = subscriptionDeltaEntries.length - 1; index >= 0; index -= 1) {
|
|
const entry = subscriptionDeltaEntries[index];
|
|
if (!countData[entry.tier]) {
|
|
countData[entry.tier] = {};
|
|
}
|
|
if (!countData[entry.tier][entry.cadence]) {
|
|
countData[entry.tier][entry.cadence] = 0;
|
|
}
|
|
|
|
subscriptionHistoryEntries.unshift({
|
|
...entry,
|
|
date: moment(entry.date).format('YYYY-MM-DD'),
|
|
count: countData[entry.tier][entry.cadence]
|
|
});
|
|
|
|
countData[entry.tier][entry.cadence] += entry.negative_delta;
|
|
countData[entry.tier][entry.cadence] -= entry.positive_delta;
|
|
|
|
if (!cadences.includes(entry.cadence)) {
|
|
cadences.push(entry.cadence);
|
|
}
|
|
if (!tiers.includes(entry.tier)) {
|
|
tiers.push(entry.tier);
|
|
}
|
|
}
|
|
|
|
return {
|
|
data: subscriptionHistoryEntries,
|
|
meta: {
|
|
cadences,
|
|
tiers,
|
|
totals: counts
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<SubscriptionDelta[]>}
|
|
**/
|
|
async fetchAllSubscriptionDeltas() {
|
|
const knex = this.knex;
|
|
const rows = await knex('members_paid_subscription_events')
|
|
.join('stripe_prices AS price', function () {
|
|
this.on('price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
|
|
.orOn('price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan');
|
|
})
|
|
.join('stripe_products AS product', 'product.stripe_product_id', '=', 'price.stripe_product_id')
|
|
.join('products AS tier', 'tier.id', '=', 'product.product_id')
|
|
.leftJoin('stripe_prices AS from_price', 'from_price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
|
|
.leftJoin('stripe_prices AS to_price', 'to_price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan')
|
|
.select(knex.raw(`
|
|
DATE(members_paid_subscription_events.created_at) as date
|
|
`))
|
|
.select(knex.raw(`
|
|
tier.id as tier
|
|
`))
|
|
.select(knex.raw(`
|
|
price.interval as cadence
|
|
`))
|
|
.select(knex.raw(`SUM(
|
|
CASE
|
|
WHEN members_paid_subscription_events.type IN ('created','reactivated') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
WHEN members_paid_subscription_events.type='updated' AND price.id = to_price.id THEN 1
|
|
ELSE 0
|
|
END
|
|
) as positive_delta`))
|
|
.select(knex.raw(`SUM(
|
|
CASE
|
|
WHEN members_paid_subscription_events.type IN ('canceled', 'expired') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
WHEN members_paid_subscription_events.type='updated' AND price.id = from_price.id THEN 1
|
|
ELSE 0
|
|
END
|
|
) as negative_delta`))
|
|
.select(knex.raw(`SUM(
|
|
CASE
|
|
WHEN members_paid_subscription_events.type IN ('created','reactivated') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
ELSE 0
|
|
END
|
|
) as signups`))
|
|
.select(knex.raw(`SUM(
|
|
CASE
|
|
WHEN members_paid_subscription_events.type IN ('canceled', 'expired') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
ELSE 0
|
|
END
|
|
) as cancellations`))
|
|
.groupBy('date', 'tier', 'cadence')
|
|
.orderBy('date');
|
|
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* Get the current total subscriptions grouped by Cadence and Tier
|
|
* @returns {Promise<SubscriptionCount[]>}
|
|
**/
|
|
async fetchSubscriptionCounts() {
|
|
const knex = this.knex;
|
|
|
|
const data = await knex('members_stripe_customers_subscriptions')
|
|
.select(knex.raw(`
|
|
COUNT(members_stripe_customers_subscriptions.id) AS count,
|
|
products.id AS tier,
|
|
stripe_prices.interval AS cadence
|
|
`))
|
|
.join('stripe_prices', 'stripe_prices.stripe_price_id', '=', 'members_stripe_customers_subscriptions.stripe_price_id')
|
|
.join('stripe_products', 'stripe_products.stripe_product_id', '=', 'stripe_prices.stripe_product_id')
|
|
.join('products', 'products.id', '=', 'stripe_products.product_id')
|
|
.whereNot('members_stripe_customers_subscriptions.mrr', 0)
|
|
.groupBy('tier', 'cadence');
|
|
|
|
return data;
|
|
}
|
|
}
|
|
|
|
/** @typedef {object} SubscriptionCount
|
|
* @prop {string} tier
|
|
* @prop {string} cadence
|
|
* @prop {number} count
|
|
**/
|
|
|
|
/**
|
|
* @typedef {object} SubscriptionDelta
|
|
* @prop {string} tier
|
|
* @prop {string} cadence
|
|
* @prop {string} date
|
|
* @prop {number} positive_delta
|
|
* @prop {number} negative_delta
|
|
* @prop {number} signups
|
|
* @prop {number} cancellations
|
|
**/
|
|
|
|
/**
|
|
* @typedef {object} SubscriptionHistoryEntry
|
|
* @prop {string} tier
|
|
* @prop {string} cadence
|
|
* @prop {string} date
|
|
* @prop {number} positive_delta
|
|
* @prop {number} negative_delta
|
|
* @prop {number} signups
|
|
* @prop {number} cancellations
|
|
* @prop {number} count
|
|
**/
|
|
|
|
module.exports = SubscriptionStatsService;
|