From 7522b74e1f9fae259dfb4172ce9a96769bb30218 Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Thu, 1 Aug 2024 16:30:50 -0700 Subject: [PATCH] Backfilled missing offer redemptions (#20647) ref https://linear.app/tryghost/issue/ENG-1440/backfill-offer-redemption-data-with-a-migration There was a bug that caused offer redemptions to not be recorded in the database for some subscriptions that were created with an offer. However, we still have the `offer_id` attached to the subscriptions, so we are able to backfill the missing redemptions. The bug was fixed in https://github.com/TryGhost/Ghost/commit/bf895e6e99e6fe8e90f0d582fa6e38d5c4e37f5d This commit only contains a migration, which queries for subscriptions that have an `offer_id` but do not have any offer redemptions recorded, and adds any missing redemptions to the `offer_redemptions` table. --- ...-30-19-51-06-backfill-offer-redemptions.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 ghost/core/core/server/data/migrations/versions/5.89/2024-07-30-19-51-06-backfill-offer-redemptions.js diff --git a/ghost/core/core/server/data/migrations/versions/5.89/2024-07-30-19-51-06-backfill-offer-redemptions.js b/ghost/core/core/server/data/migrations/versions/5.89/2024-07-30-19-51-06-backfill-offer-redemptions.js new file mode 100644 index 000000000000..b37b1e3ab729 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.89/2024-07-30-19-51-06-backfill-offer-redemptions.js @@ -0,0 +1,64 @@ +// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253 + +const logging = require('@tryghost/logging'); +const DatabaseInfo = require('@tryghost/database-info'); +const {default: ObjectID} = require('bson-objectid'); + +// For DML - data changes +const {createTransactionalMigration} = require('../../utils'); + +module.exports = createTransactionalMigration( + async function up(knex) { + // Backfill missing offer redemptions + try { + // Select all subscriptions that have an `offer_id` but don't have a matching row in the `offer_redemptions` table + logging.info('Selecting subscriptions with missing offer redemptions'); + const result = await knex.raw(` + SELECT + mscs.id AS subscription_id, + mscs.offer_id, + mscs.start_date AS created_at, + m.id AS member_id + FROM + members_stripe_customers_subscriptions mscs + LEFT JOIN + offer_redemptions r ON r.subscription_id = mscs.id + INNER JOIN + members_stripe_customers msc ON mscs.customer_id = msc.customer_id + INNER JOIN + members m ON msc.member_id = m.id + WHERE + mscs.offer_id IS NOT NULL and r.id IS NULL; + `); + + // knex.raw() returns a different result depending on the database. We need to handle either case + let rows = []; + if (DatabaseInfo.isSQLite(knex)) { + rows = result; + } else { + rows = result[0]; + } + + // Do the backfil + if (rows && rows.length > 0) { + logging.info(`Backfilling ${rows.length} offer redemptions`); + // Generate IDs for each row + const offerRedemptions = rows.map((row) => { + return { + id: (new ObjectID()).toHexString(), + ...row + }; + }); + // Batch insert rows into the offer_redemptions table + await knex.batchInsert('offer_redemptions', offerRedemptions, 1000); + } else { + logging.info('No offer redemptions to backfill'); + } + } catch (error) { + logging.error(`Error backfilling offer redemptions: ${error.message}`); + } + }, + async function down() { + // We don't want to un-backfill data, so do nothing here. + } +); \ No newline at end of file