0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00: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],
avatar_image: req.member.avatar_image,
subscriptions: req.member.stripe.subscriptions,
paid: req.member.stripe.subscriptions.filter((subscription) => {
return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.status);
}).length !== 0
paid: req.member.status === 'paid'
} : null;
hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, {

View file

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

View file

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

View file

@ -1,7 +1,6 @@
const ghostBookshelf = require('./base');
const uuid = require('uuid');
const _ = require('lodash');
const {sequence} = require('@tryghost/promise');
const config = require('../../shared/config');
const crypto = require('crypto');
@ -108,7 +107,6 @@ const Member = ghostBookshelf.Model.extend({
onSaving: function onSaving(model, attr, options) {
let labelsToSave = [];
let ops = [];
// CASE: detect lowercase/uppercase label slugs
if (!_.isUndefined(this.get('labels')) && !_.isNull(this.get('labels'))) {
@ -129,26 +127,23 @@ const Member = ghostBookshelf.Model.extend({
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);
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) {
@ -209,40 +204,6 @@ const Member = ghostBookshelf.Model.extend({
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) {
if (field === 'email_open_rate') {
return {
@ -276,8 +237,7 @@ const Member = ghostBookshelf.Model.extend({
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
if (['findPage', 'findAll'].includes(methodName)) {
// TODO: remove 'paid' once it's possible to use in a filter
options = options.concat(['search', 'paid']);
options = options.concat(['search']);
}
return options;

View file

@ -92,18 +92,19 @@ const sendTestEmail = async (postModel, toEmails) => {
const addEmail = async (postModel, options) => {
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');
switch (emailRecipientFilter) {
case 'paid':
filterOptions.paid = true;
filterOptions.filter = 'subscribed:true+status:paid';
break;
case 'free':
filterOptions.paid = false;
filterOptions.filter = 'subscribed:true+status:free';
break;
case 'all':
filterOptions.filter = 'subscribed:true';
break;
case 'none':
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']);
// 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');
switch (recipientFilter) {
case 'paid':
filterOptions.paid = true;
filterOptions.filter = 'subscribed:true+status:paid';
break;
case 'free':
filterOptions.paid = false;
filterOptions.filter = 'subscribed:true+status:free';
break;
case 'all':
filterOptions.filter = 'subscribed:true';
break;
default:
throw new Error(`Unknown recipient_filter ${recipientFilter}`);

View file

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

View file

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

View file

@ -57,7 +57,7 @@
"@tryghost/kg-mobiledoc-html-renderer": "3.0.1",
"@tryghost/magic-link": "0.6.4",
"@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-ssr": "0.8.8",
"@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');
});
it('Can browse with paid', async function () {
it('Can filter by paid status', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/?paid=true'))
.get(localUtils.API.getApiQuery('members/?filter=status:paid'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)

View file

@ -210,20 +210,6 @@ describe('Member Model', function run() {
describe('findAll', function () {
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) {
Member.findAll({search: 'egg'}).then(function (queryResult) {
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>');
});
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 = {
visibility: 'paid',
plaintext: 'I see dead people',
@ -97,9 +97,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
original: {
context: {
member: {
stripe: {
subscriptions: []
}
status: 'free'
}
}
}
@ -111,35 +109,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
attrs.html.should.eql('');
});
it('should hide content attributes when visibility is "paid" and member has cancelled subscription', 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 () {
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
@ -151,11 +121,7 @@ describe('Unit: canary/utils/serializers/output/utils/post-gating', function ()
original: {
context: {
member: {
stripe: {
subscriptions: [{
status: 'active'
}]
}
status: 'paid'
}
}
}

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>');
});
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 = {
visibility: 'paid',
plaintext: 'I see dead people',
@ -93,9 +93,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
original: {
context: {
member: {
stripe: {
subscriptions: []
}
status: 'free'
}
}
}
@ -107,35 +105,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
attrs.html.should.eql('');
});
it('should hide content attributes when visibility is "paid" and member has cancelled subscription', 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 () {
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
@ -147,11 +117,7 @@ describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
original: {
context: {
member: {
stripe: {
subscriptions: [{
status: 'active'
}]
}
status: 'paid'
}
}
}

View file

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

View file

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