2575e10ec4
refs https://github.com/TryGhost/Team/issues/1505 refs https://github.com/TryGhost/Team/issues/1466 This gives us historic data for subscriptions broken down by tier and cadence * Cleaned up tests Fixed up usage of knex, making sure to destroy the connection after tests * Removed Node 12 from testing matrix Get outta my pub!
143 lines
5.1 KiB
JavaScript
143 lines
5.1 KiB
JavaScript
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 = [];
|
|
|
|
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,
|
|
count: countData[entry.tier][entry.cadence]
|
|
});
|
|
|
|
countData[entry.tier][entry.cadence] += entry.negative_delta;
|
|
countData[entry.tier][entry.cadence] -= entry.positive_delta;
|
|
}
|
|
|
|
return {
|
|
data: subscriptionHistoryEntries
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @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='created' 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`))
|
|
.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
|
|
**/
|
|
|
|
/**
|
|
* @typedef {object} SubscriptionHistoryEntry
|
|
* @prop {string} tier
|
|
* @prop {string} cadence
|
|
* @prop {string} date
|
|
* @prop {number} positive_delta
|
|
* @prop {number} negative_delta
|
|
* @prop {number} count
|
|
**/
|
|
|
|
module.exports = SubscriptionStatsService;
|