0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Handled storing complimentary subscription expiry

refs https://github.com/TryGhost/Team/issues/1727

- if feature flag is enabled, handles storing expiry date on complimentary subscriptions in `expiry_at` column of `members_products`
- updates the expiry value on both member edit or add with tiers
- expiry is passed as `expiry_at` in `tiers` list of a member
- includes `expiry_at` on tiers data of a member when flag is enabled
This commit is contained in:
Rishabh 2022-08-19 16:20:36 +05:30 committed by Rishabh Garg
parent c123fdf5da
commit 1258156c38
4 changed files with 82 additions and 18 deletions

View file

@ -3,6 +3,7 @@ const uuid = require('uuid');
const _ = require('lodash');
const config = require('../../shared/config');
const {gravatar} = require('../lib/image');
const labs = require('../../shared/labs');
const Member = ghostBookshelf.Model.extend({
tableName: 'members',
@ -102,12 +103,16 @@ const Member = ghostBookshelf.Model.extend({
products() {
return this.belongsToMany('Product', 'members_products', 'member_id', 'product_id')
.withPivot('sort_order')
.withPivot('sort_order', 'expiry_at')
.query('orderBy', 'sort_order', 'ASC')
.query((qb) => {
// avoids bookshelf adding a `DISTINCT` to the query
// we know the result set will already be unique and DISTINCT hurts query performance
qb.columns('products.*');
if (labs.isSet('compExpiring')) {
qb.columns('products.*', 'expiry_at');
} else {
qb.columns('products.*');
}
});
},
@ -155,6 +160,21 @@ const Member = ghostBookshelf.Model.extend({
return this.hasMany('EmailRecipient', 'member_id', 'id');
},
async updateTierExpiry(products = [], options = {}) {
if (!labs.isSet('compExpiring')) {
return;
}
for (const product of products) {
if (product?.expiry_at) {
const expiry = new Date(product.expiry_at);
const queryOptions = _.extend({}, options, {
query: {where: {product_id: product.id}}
});
await this.products().updatePivot({expiry_at: expiry}, queryOptions);
}
}
},
serialize(options) {
const defaultSerializedObject = ghostBookshelf.Model.prototype.serialize.call(this, options);
@ -344,7 +364,13 @@ const Member = ghostBookshelf.Model.extend({
return this.add(data, Object.assign({transacting}, unfilteredOptions));
});
}
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions);
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions).then(async (member) => {
if (data.products) {
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
}
return member;
});
},
edit(data, unfilteredOptions = {}) {
@ -353,7 +379,13 @@ const Member = ghostBookshelf.Model.extend({
return this.edit(data, Object.assign({transacting}, unfilteredOptions));
});
}
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions);
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions).then(async (member) => {
if (data.products) {
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
}
return member;
});
},
destroy(unfilteredOptions = {}) {

View file

@ -63,7 +63,7 @@ exports[`Members API - With Newsletters - compat mode Can fetch members who are
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2071",
"content-length": "2088",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -218,7 +218,7 @@ exports[`Members API - With Newsletters - compat mode Can fetch members who are
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "11646",
"content-length": "11697",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -289,7 +289,7 @@ exports[`Members API - With Newsletters Can fetch members who are NOT subscribed
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2071",
"content-length": "2088",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -444,7 +444,7 @@ exports[`Members API - With Newsletters Can fetch members who are subscribed 2:
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "11646",
"content-length": "11697",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",

View file

@ -905,6 +905,7 @@ Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"expiry_at": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Product",
@ -928,7 +929,7 @@ exports[`Members API Can add complimentary subscription (out of date) 4: [header
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2583",
"content-length": "2617",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1125,7 +1126,7 @@ exports[`Members API Can browse 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "13616",
"content-length": "13684",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1286,6 +1287,7 @@ Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"expiry_at": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Product",
@ -1309,7 +1311,7 @@ exports[`Members API Can create a member with an existing complimentary subscrip
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2634",
"content-length": "2668",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -1378,7 +1380,7 @@ exports[`Members API Can create a member with an existing paid subscription 2: [
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2620",
"content-length": "2654",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -1440,6 +1442,7 @@ Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"expiry_at": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Product",
@ -1463,7 +1466,7 @@ exports[`Members API Can create a new member with a product (complementary) 2: [
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2410",
"content-length": "2444",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -1826,7 +1829,7 @@ exports[`Members API Can filter by paid status 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9935",
"content-length": "10003",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1939,7 +1942,7 @@ exports[`Members API Can filter on newsletter slug 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "8642",
"content-length": "8676",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2144,7 +2147,7 @@ exports[`Members API Can filter on tier slug 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "20695",
"content-length": "20967",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2328,7 +2331,7 @@ exports[`Members API Can ignore any unknown includes 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9935",
"content-length": "10003",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -3494,7 +3497,7 @@ exports[`Members API Search for paid members retrieves member with email paid@te
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2486",
"content-length": "2503",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",

View file

@ -1,6 +1,8 @@
const sinon = require('sinon');
const should = require('should');
const models = require('../../../../core/server/models');
const configUtils = require('../../../utils/configUtils');
const labs = require('../../../../core/shared/labs');
const config = configUtils.config;
@ -49,4 +51,31 @@ describe('Unit: models/member', function () {
should(json.avatar_image).eql(null);
});
});
describe('updateTierExpiry', function () {
let memberModel;
let updatePivot;
beforeEach(function () {
memberModel = new models.Member({email: 'text@example.com'});
updatePivot = sinon.stub();
sinon.stub(memberModel, 'products').callsFake(() => {
return {
updatePivot: updatePivot
};
});
sinon.stub(labs, 'isSet').returns(true);
});
it('calls updatePivot on member products to set expiry', function () {
const expiry = (new Date()).toISOString();
memberModel.updateTierExpiry([{
expiry_at: expiry,
id: '1'
}]);
updatePivot.calledWith({expiry_at: new Date(expiry)}, {query: {where: {product_id: '1'}}}).should.be.true();
});
});
});