mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 22:11:09 +03:00
🐛 Fixed invalid expiry for member tier subscriptions (#16174)
refs https://github.com/TryGhost/Team/issues/2476 When upgrading from a Complimentary subscription with an expiry, to a paid Subscription of the same Tier, the Member was eventually losing access to the Tier when the complimentary subscription expires as the `expiry_at` on the mapping was not removed. This change fixes the code by setting expiry as null when a member upgrades their subscription to paid. This also adds 2 migrations to fix any side-effects on existing sites - - Removed invalid expiry tier expiry date for paid members - Restored missing tier mapping for paid members
This commit is contained in:
parent
1d32931d0a
commit
503a9ebe51
@ -0,0 +1,35 @@
|
||||
const logging = require('@tryghost/logging');
|
||||
const {createTransactionalMigration} = require('../../utils');
|
||||
|
||||
module.exports = createTransactionalMigration(
|
||||
async function up(knex) {
|
||||
logging.info('Removing expiry dates for paid members');
|
||||
try {
|
||||
// Fetch all members with a paid status that have an expiry date
|
||||
// Paid members should not have an expiry date
|
||||
const invalidExpiryIds = await knex('members_products')
|
||||
.select('members_products.id')
|
||||
.leftJoin('members', 'members_products.member_id', 'members.id')
|
||||
.where('members.status', '=', 'paid')
|
||||
.whereNotNull('members_products.expiry_at').pluck('members_products.id');
|
||||
|
||||
logging.info(`Found ${invalidExpiryIds.length} paid members with expiry dates`);
|
||||
|
||||
if (invalidExpiryIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logging.info(`Removing expiry dates for ${invalidExpiryIds.length} paid members`);
|
||||
|
||||
await knex('members_products')
|
||||
.update('expiry_at', null)
|
||||
.whereIn('id', invalidExpiryIds);
|
||||
} catch (err) {
|
||||
logging.warn('Failed to remove expiry dates for paid members');
|
||||
logging.warn(err);
|
||||
}
|
||||
},
|
||||
async function down() {
|
||||
// no-op: we don't want to reintroduce the incorrect expiry dates for member tiers
|
||||
}
|
||||
);
|
@ -0,0 +1,46 @@
|
||||
const logging = require('@tryghost/logging');
|
||||
const ObjectId = require('bson-objectid').default;
|
||||
const {createTransactionalMigration} = require('../../utils');
|
||||
|
||||
module.exports = createTransactionalMigration(
|
||||
async function up(knex) {
|
||||
logging.info('Restoring member<>tier mapping for members with paid status');
|
||||
try {
|
||||
// fetch all members with a paid status that don't have a members_products record
|
||||
// and have a members_product_events record with an action of "added"
|
||||
// and fetch the product_id from the most recent record for that member
|
||||
const memberWithTiers = await knex.select('m.id as member_id', 'mpe.product_id as product_id')
|
||||
.from('members as m')
|
||||
.leftJoin('members_products as mp', 'm.id', 'mp.member_id')
|
||||
.leftJoin('members_product_events as mpe', function () {
|
||||
this.on('m.id', 'mpe.member_id')
|
||||
.andOn(knex.raw('mpe.created_at = (SELECT max(created_at) FROM members_product_events WHERE member_id = mpe.member_id and action = "added")'));
|
||||
})
|
||||
.where({'m.status': 'paid', 'mp.member_id': null, 'mpe.action': 'added'});
|
||||
|
||||
// create a new members_products record for each member with id, member_id and product_id
|
||||
const toInsert = memberWithTiers.map((memberTier) => {
|
||||
return {
|
||||
...memberTier,
|
||||
id: ObjectId().toHexString()
|
||||
};
|
||||
}).filter((memberTier) => {
|
||||
// filter out any members that don't have a product_id for some reason
|
||||
if (!memberTier.product_id) {
|
||||
logging.warn(`Invalid record found - member_id: ${memberTier.member_id} is without product_id`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
logging.info(`Inserting ${toInsert.length} records into members_products`);
|
||||
await knex.batchInsert('members_products', toInsert);
|
||||
} catch (err) {
|
||||
logging.warn('Failed to restore member<>tier mapping for members with paid status');
|
||||
logging.warn(err);
|
||||
}
|
||||
},
|
||||
async function down() {
|
||||
// np-op: we don't want to delete the missing records we've just inserted
|
||||
}
|
||||
);
|
@ -219,8 +219,8 @@ const Member = ghostBookshelf.Model.extend({
|
||||
|
||||
async updateTierExpiry(products = [], options = {}) {
|
||||
for (const product of products) {
|
||||
if (product?.expiry_at) {
|
||||
const expiry = new Date(product.expiry_at);
|
||||
if (product?.id) {
|
||||
const expiry = product.expiry_at ? new Date(product.expiry_at) : null;
|
||||
const queryOptions = _.extend({}, options, {
|
||||
query: {where: {product_id: product.id}}
|
||||
});
|
||||
|
@ -77,5 +77,13 @@ describe('Unit: models/member', function () {
|
||||
|
||||
updatePivot.calledWith({expiry_at: new Date(expiry)}, {query: {where: {product_id: '1'}}}).should.be.true();
|
||||
});
|
||||
|
||||
it('calls updatePivot on member products to remove expiry', function () {
|
||||
memberModel.updateTierExpiry([{
|
||||
id: '1'
|
||||
}]);
|
||||
|
||||
updatePivot.calledWith({expiry_at: null}, {query: {where: {product_id: '1'}}}).should.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user