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

Updated Admin API and Mega to use status flag (#12579)

no-issue

* Removed support for paid param from v3 & canary API
* Updated active subscription checks to use status flag
* Updated MEGA to use status filter over paid flag
* Removed support for paid option at model level
* Installed @tryghost/members-api@1.0.0-rc.0
* Updated members fixtures
This commit is contained in:
Fabien 'egg' O'Carroll 2021-01-28 16:31:02 +00:00 committed by Daniel Lockyer
parent 229295d671
commit 6af2706f10
No known key found for this signature in database
GPG key ID: FFBC6FA2A6F6ABC1
14 changed files with 57 additions and 186 deletions

View file

@ -133,9 +133,7 @@ function updateLocalTemplateOptions(req, res, next) {
firstname: req.member.name && req.member.name.split(' ')[0], firstname: req.member.name && req.member.name.split(' ')[0],
avatar_image: req.member.avatar_image, avatar_image: req.member.avatar_image,
subscriptions: req.member.stripe.subscriptions, subscriptions: req.member.stripe.subscriptions,
paid: req.member.stripe.subscriptions.filter((subscription) => { paid: req.member.status === 'paid'
return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.status);
}).length !== 0
} : null; } : null;
hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, { hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, {

View file

@ -34,8 +34,7 @@ module.exports = {
'order', 'order',
'debug', 'debug',
'page', 'page',
'search', 'search'
'paid'
], ],
permissions: true, permissions: true,
validation: {}, validation: {},
@ -304,8 +303,7 @@ module.exports = {
options: [ options: [
'limit', 'limit',
'filter', 'filter',
'search', 'search'
'paid'
], ],
headers: { headers: {
disposition: { disposition: {

View file

@ -34,8 +34,7 @@ module.exports = {
'order', 'order',
'debug', 'debug',
'page', 'page',
'search', 'search'
'paid'
], ],
permissions: true, permissions: true,
validation: {}, validation: {},
@ -304,8 +303,7 @@ module.exports = {
options: [ options: [
'limit', 'limit',
'filter', 'filter',
'search', 'search'
'paid'
], ],
headers: { headers: {
disposition: { disposition: {

View file

@ -1,7 +1,6 @@
const ghostBookshelf = require('./base'); const ghostBookshelf = require('./base');
const uuid = require('uuid'); const uuid = require('uuid');
const _ = require('lodash'); const _ = require('lodash');
const {sequence} = require('@tryghost/promise');
const config = require('../../shared/config'); const config = require('../../shared/config');
const crypto = require('crypto'); const crypto = require('crypto');
@ -108,7 +107,6 @@ const Member = ghostBookshelf.Model.extend({
onSaving: function onSaving(model, attr, options) { onSaving: function onSaving(model, attr, options) {
let labelsToSave = []; let labelsToSave = [];
let ops = [];
// CASE: detect lowercase/uppercase label slugs // CASE: detect lowercase/uppercase label slugs
if (!_.isUndefined(this.get('labels')) && !_.isNull(this.get('labels'))) { if (!_.isUndefined(this.get('labels')) && !_.isNull(this.get('labels'))) {
@ -129,26 +127,23 @@ const Member = ghostBookshelf.Model.extend({
this.set('labels', labelsToSave); this.set('labels', labelsToSave);
} }
// CASE: Detect existing labels with same case-insensitive name and replace
ops.push(function updateLabels() {
return ghostBookshelf.model('Label')
.findAll(Object.assign({
columns: ['id', 'name']
}, _.pick(options, 'transacting')))
.then((labels) => {
labelsToSave.forEach((label) => {
let existingLabel = labels.find((lab) => {
return label.name.toLowerCase() === lab.get('name').toLowerCase();
});
label.name = (existingLabel && existingLabel.get('name')) || label.name;
});
model.set('labels', labelsToSave);
});
});
this.handleAttachedModels(model); this.handleAttachedModels(model);
return sequence(ops);
// CASE: Detect existing labels with same case-insensitive name and replace
return ghostBookshelf.model('Label')
.findAll(Object.assign({
columns: ['id', 'name']
}, _.pick(options, 'transacting')))
.then((labels) => {
labelsToSave.forEach((label) => {
let existingLabel = labels.find((lab) => {
return label.name.toLowerCase() === lab.get('name').toLowerCase();
});
label.name = (existingLabel && existingLabel.get('name')) || label.name;
});
model.set('labels', labelsToSave);
});
}, },
handleAttachedModels: function handleAttachedModels(model) { handleAttachedModels: function handleAttachedModels(model) {
@ -209,40 +204,6 @@ const Member = ghostBookshelf.Model.extend({
queryBuilder.orWhere('members.email', 'like', `%${query}%`); queryBuilder.orWhere('members.email', 'like', `%${query}%`);
}, },
// TODO: hacky way to filter by members with an active subscription,
// replace with a proper way to do this via filter param.
// NOTE: assumes members will have a single subscription
customQuery: function customQuery(queryBuilder, options) {
if (options.paid === true) {
queryBuilder.innerJoin(
'members_stripe_customers',
'members.id',
'members_stripe_customers.member_id'
);
queryBuilder.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']
);
}
);
}
if (options.paid === false) {
queryBuilder.leftJoin(
'members_stripe_customers',
'members.id',
'members_stripe_customers.member_id'
);
queryBuilder.whereNull('members_stripe_customers.member_id');
}
},
orderRawQuery(field, direction) { orderRawQuery(field, direction) {
if (field === 'email_open_rate') { if (field === 'email_open_rate') {
return { return {
@ -276,8 +237,7 @@ const Member = ghostBookshelf.Model.extend({
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName); let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
if (['findPage', 'findAll'].includes(methodName)) { if (['findPage', 'findAll'].includes(methodName)) {
// TODO: remove 'paid' once it's possible to use in a filter options = options.concat(['search']);
options = options.concat(['search', 'paid']);
} }
return options; return options;

View file

@ -92,18 +92,19 @@ const sendTestEmail = async (postModel, toEmails) => {
const addEmail = async (postModel, options) => { const addEmail = async (postModel, options) => {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']); const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
const filterOptions = Object.assign({}, knexOptions, {filter: 'subscribed:true', limit: 1}); const filterOptions = Object.assign({}, knexOptions, {limit: 1});
const emailRecipientFilter = postModel.get('email_recipient_filter'); const emailRecipientFilter = postModel.get('email_recipient_filter');
switch (emailRecipientFilter) { switch (emailRecipientFilter) {
case 'paid': case 'paid':
filterOptions.paid = true; filterOptions.filter = 'subscribed:true+status:paid';
break; break;
case 'free': case 'free':
filterOptions.paid = false; filterOptions.filter = 'subscribed:true+status:free';
break; break;
case 'all': case 'all':
filterOptions.filter = 'subscribed:true';
break; break;
case 'none': case 'none':
throw new Error('Cannot sent email to "none" email_recipient_filter'); throw new Error('Cannot sent email to "none" email_recipient_filter');
@ -288,18 +289,19 @@ async function getEmailMemberRows({emailModel, options}) {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']); const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
// TODO: this will clobber a user-assigned filter if/when we allow emails to be sent to filtered member lists // TODO: this will clobber a user-assigned filter if/when we allow emails to be sent to filtered member lists
const filterOptions = Object.assign({}, knexOptions, {filter: 'subscribed:true'}); const filterOptions = Object.assign({}, knexOptions);
const recipientFilter = emailModel.get('recipient_filter'); const recipientFilter = emailModel.get('recipient_filter');
switch (recipientFilter) { switch (recipientFilter) {
case 'paid': case 'paid':
filterOptions.paid = true; filterOptions.filter = 'subscribed:true+status:paid';
break; break;
case 'free': case 'free':
filterOptions.paid = false; filterOptions.filter = 'subscribed:true+status:free';
break; break;
case 'all': case 'all':
filterOptions.filter = 'subscribed:true';
break; break;
default: default:
throw new Error(`Unknown recipient_filter ${recipientFilter}`); throw new Error(`Unknown recipient_filter ${recipientFilter}`);

View file

@ -23,12 +23,7 @@ function checkPostAccess(post, member) {
return PERMIT_ACCESS; return PERMIT_ACCESS;
} }
const activeSubscriptions = member.stripe && member.stripe.subscriptions && member.stripe.subscriptions.filter((subscription) => { if (post.visibility === 'paid' && member.status === 'paid') {
return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.status);
});
const memberHasPlan = activeSubscriptions && activeSubscriptions.length;
if (post.visibility === 'paid' && memberHasPlan) {
return PERMIT_ACCESS; return PERMIT_ACCESS;
} }

View file

@ -10,8 +10,6 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member
avatar_image: member.avatar_image, avatar_image: member.avatar_image,
subscribed: !!member.subscribed, subscribed: !!member.subscribed,
subscriptions: member.stripe ? member.stripe.subscriptions : [], subscriptions: member.stripe ? member.stripe.subscriptions : [],
paid: member.stripe && member.stripe.subscriptions && member.stripe.subscriptions.filter((subscription) => { paid: member.status === 'paid'
return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.status);
}).length !== 0
}; };
}; };

View file

@ -57,7 +57,7 @@
"@tryghost/kg-mobiledoc-html-renderer": "3.0.1", "@tryghost/kg-mobiledoc-html-renderer": "3.0.1",
"@tryghost/magic-link": "0.6.4", "@tryghost/magic-link": "0.6.4",
"@tryghost/maintenance": "0.1.0", "@tryghost/maintenance": "0.1.0",
"@tryghost/members-api": "0.37.7", "@tryghost/members-api": "1.0.0-rc.0",
"@tryghost/members-csv": "0.4.2", "@tryghost/members-csv": "0.4.2",
"@tryghost/members-ssr": "0.8.8", "@tryghost/members-ssr": "0.8.8",
"@tryghost/mw-session-from-token": "0.1.14", "@tryghost/mw-session-from-token": "0.1.14",

View file

@ -87,9 +87,9 @@ describe('Members API', function () {
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
}); });
it('Can browse with paid', async function () { it('Can filter by paid status', async function () {
const res = await request const res = await request
.get(localUtils.API.getApiQuery('members/?paid=true')) .get(localUtils.API.getApiQuery('members/?filter=status:paid'))
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)

View file

@ -210,20 +210,6 @@ describe('Member Model', function run() {
describe('findAll', function () { describe('findAll', function () {
beforeEach(testUtils.setup('members')); beforeEach(testUtils.setup('members'));
it('can use custom query', function (done) {
Member.findAll().then(function (allResult) {
allResult.length.should.equal(4);
return Member.findAll({paid: true});
}).then(function (queryResult) {
queryResult.length.should.equal(2);
queryResult.models[0].get('email').should.equal('paid@test.com');
queryResult.models[1].get('email').should.equal('trialing@test.com');
done();
}).catch(done);
});
it('can use search query', function (done) { it('can use search query', function (done) {
Member.findAll({search: 'egg'}).then(function (queryResult) { Member.findAll({search: 'egg'}).then(function (queryResult) {
queryResult.length.should.equal(1); queryResult.length.should.equal(1);

View file

@ -85,7 +85,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
attrs.html.should.eql('<p>What\'s the matter?</p>'); attrs.html.should.eql('<p>What\'s the matter?</p>');
}); });
it('should hide content attributes when visibility is "paid" and member has no subscription', function () { it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = { const attrs = {
visibility: 'paid', visibility: 'paid',
plaintext: 'I see dead people', plaintext: 'I see dead people',
@ -97,9 +97,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
original: { original: {
context: { context: {
member: { member: {
stripe: { status: 'free'
subscriptions: []
}
} }
} }
} }
@ -111,35 +109,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
attrs.html.should.eql(''); attrs.html.should.eql('');
}); });
it('should hide content attributes when visibility is "paid" and member has cancelled subscription', function () { it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
options: {},
original: {
context: {
member: {
stripe: {
subscriptions: [{
status: 'canceled'
}]
}
}
}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "paid" and member has a subscription', function () {
const attrs = { const attrs = {
visibility: 'paid', visibility: 'paid',
plaintext: 'Secret paid content', plaintext: 'Secret paid content',
@ -151,11 +121,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
original: { original: {
context: { context: {
member: { member: {
stripe: { status: 'paid'
subscriptions: [{
status: 'active'
}]
}
} }
} }
} }

View file

@ -82,7 +82,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
attrs.html.should.eql('<p>What\'s the matter?</p>'); attrs.html.should.eql('<p>What\'s the matter?</p>');
}); });
it('should hide content attributes when visibility is "paid" and member has no subscription', function () { it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = { const attrs = {
visibility: 'paid', visibility: 'paid',
plaintext: 'I see dead people', plaintext: 'I see dead people',
@ -93,9 +93,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
original: { original: {
context: { context: {
member: { member: {
stripe: { status: 'free'
subscriptions: []
}
} }
} }
} }
@ -107,35 +105,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
attrs.html.should.eql(''); attrs.html.should.eql('');
}); });
it('should hide content attributes when visibility is "paid" and member has cancelled subscription', function () { it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
options: {},
original: {
context: {
member: {
stripe: {
subscriptions: [{
status: 'canceled'
}]
}
}
}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "paid" and member has a subscription', function () {
const attrs = { const attrs = {
visibility: 'paid', visibility: 'paid',
plaintext: 'Secret paid content', plaintext: 'Secret paid content',
@ -147,11 +117,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
original: { original: {
context: { context: {
member: { member: {
stripe: { status: 'paid'
subscriptions: [{
status: 'active'
}]
}
} }
} }
} }

View file

@ -311,26 +311,30 @@ DataGenerator.Content = {
id: ObjectId.generate(), id: ObjectId.generate(),
email: 'member1@test.com', email: 'member1@test.com',
name: 'Mr Egg', name: 'Mr Egg',
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b340' uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b340',
status: 'free'
}, },
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
email: 'member2@test.com', email: 'member2@test.com',
email_open_rate: 50, email_open_rate: 50,
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b341' uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b341',
status: 'free'
}, },
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
email: 'paid@test.com', email: 'paid@test.com',
name: 'Egon Spengler', name: 'Egon Spengler',
email_open_rate: 80, email_open_rate: 80,
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b342' uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b342',
status: 'paid'
}, },
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
email: 'trialing@test.com', email: 'trialing@test.com',
name: 'Ray Stantz', name: 'Ray Stantz',
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343' uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343',
status: 'paid'
} }
], ],

View file

@ -553,17 +553,17 @@
dependencies: dependencies:
ghost-ignition "^4.4.3" ghost-ignition "^4.4.3"
"@tryghost/members-api@0.37.7": "@tryghost/members-api@1.0.0-rc.0":
version "0.37.7" version "1.0.0-rc.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.37.7.tgz#95102f775a0647898cf26919eb543a5bd0aa7778" resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-1.0.0-rc.0.tgz#e7769b8e886ea32c975e507268a1677b1af890a2"
integrity sha512-bE/5MO2OPak3OhJK4piz+U9ocnYRTMM/7zBYIOtTDCvGBmCsEUoChUEHNqHHrcE9Iw/cYcdPPqgtlcGkXbCgug== integrity sha512-gHLwgyXl5wXlvNvtRYA6N4Nqod+oWEIa7g62lpZQCUQFeJkSZT1UKwTCCTs6Rd3CPjxo0QVk5dMb3y1vxLc/mg==
dependencies: dependencies:
"@tryghost/magic-link" "^0.6.6" "@tryghost/magic-link" "^0.6.5"
bluebird "^3.5.4" bluebird "^3.5.4"
body-parser "^1.19.0" body-parser "^1.19.0"
cookies "^0.8.0" cookies "^0.8.0"
express "^4.16.4" express "^4.16.4"
ghost-ignition "4.4.3" ghost-ignition "4.4.2"
got "^9.6.0" got "^9.6.0"
jsonwebtoken "^8.5.1" jsonwebtoken "^8.5.1"
leaky-bucket "2.2.0" leaky-bucket "2.2.0"