From a0a35df13bf7410001e2ca6423b7b5c7b24a411b Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Mon, 6 Sep 2021 18:56:44 +0200 Subject: [PATCH] Migrated members comped status to reflect subscriptions (#13285) * Migrated members comped status to reflect subscriptions refs https://github.com/TryGhost/Team/issues/995 Due to a bug in subscription handling, members with Complimentary stripe subscriptions were incorrectly given a status of 'paid'. The goal of this migration is to fix existing broken members, and it will be accompanied by a fix which prevents the bug for future members. Since we are updating the status properties for members, we must ensure that we also update the relevant member_status_events entries too, so that we do not have incompatible sums between events and statuses. For example, if we were to use events to graph comped members over time, we would want the current count to match the query on statuses: `SELECT COUNT(*) FROM members WHERE status='comped';` --- .../4.14/01-fix-comped-member-statuses.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js diff --git a/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js b/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js new file mode 100644 index 0000000000..94840bf7f7 --- /dev/null +++ b/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js @@ -0,0 +1,70 @@ +const {chunk} = require('lodash'); +const {createTransactionalMigration} = require('../../utils'); +const logging = require('@tryghost/logging'); + +module.exports = createTransactionalMigration(async function up(knex) { + const compedMemberIds = (await knex('members') + .select('members.id') + .innerJoin( + 'members_stripe_customers', + 'members.id', + 'members_stripe_customers.member_id' + ).innerJoin( + 'members_stripe_customers_subscriptions', + function () { + this.on( + 'members_stripe_customers.customer_id', + 'members_stripe_customers_subscriptions.customer_id' + ).onIn( + 'members_stripe_customers_subscriptions.status', + ['active', 'trialing', 'past_due', 'unpaid'] + ); + } + ).where( + 'members_stripe_customers_subscriptions.plan_nickname', + '=', + 'Complimentary' + ).andWhere( + 'members.status', + '!=', + 'comped' + )).map(({id}) => id); + + if (!compedMemberIds.length) { + logging.warn('No Complimentary members found with incorrect status'); + return; + } else { + logging.info(`Found ${compedMemberIds.length} Complimentary members with the incorrect status`); + } + + // Umm? Well... The current version of SQLite3 bundled with Ghost supports + // a maximum of 999 variables, we use one variable for the SET value + // and so we're left with 998 for our WHERE IN clause values + const chunkSize = 998; + + const compedMemberIdChunks = chunk(compedMemberIds, chunkSize); + + for (const compedMemberIdsChunk of compedMemberIdChunks) { + await knex('members') + .update('status', 'comped') + .whereIn('id', compedMemberIdsChunk); + } + + for (const memberId of compedMemberIds) { + const mostRecentStatusEvent = await knex('members_status_events') + .select('*') + .where('member_id', memberId) + .orderBy('created_at', 'desc') + .limit(1) + .first(); + + if (!mostRecentStatusEvent) { + logging.warn(`Could not find a status event for member ${memberId} - skipping this member`); + } else if (mostRecentStatusEvent.to_status !== 'comped') { + logging.info(`Updating members_status_event ${mostRecentStatusEvent.id}`); + await knex('members_status_events') + .update('to_status', 'comped') + .where('id', mostRecentStatusEvent.id); + } + } +}, async function down() {});