diff --git a/ghost/members-api/index.js b/ghost/members-api/index.js index d3c83dbeec..9b32765905 100644 --- a/ghost/members-api/index.js +++ b/ghost/members-api/index.js @@ -154,6 +154,12 @@ module.exports = function MembersApi({ return res.end('Bad Request.'); } + // NOTE: never allow "Complimenatry" plan to be subscribed to from the client + if (plan.toLowerCase() === 'complimentary') { + res.writeHead(400); + return res.end('Bad Request.'); + } + let email; try { if (!identity) { @@ -276,6 +282,11 @@ module.exports = function MembersApi({ return res.end('No permission'); } + if (subscription.plan.nickname === 'Complimentary') { + res.writeHead(400); + return res.end('Bad request'); + } + if (cancelAtPeriodEnd === undefined) { throw new common.errors.BadRequestError({ message: 'Canceling membership failed!', diff --git a/ghost/members-api/lib/stripe/index.js b/ghost/members-api/lib/stripe/index.js index 6dc1ccb638..66827a25f2 100644 --- a/ghost/members-api/lib/stripe/index.js +++ b/ghost/members-api/lib/stripe/index.js @@ -128,9 +128,10 @@ module.exports = class StripePaymentProcessor { return subscription.status !== 'canceled'; }); - await Promise.all(activeSubscriptions.map((subscription) => { - return del(this._stripe, 'subscriptions', subscription.id); - })); + for (const subscription of activeSubscriptions) { + const updatedSubscription = await del(this._stripe, 'subscriptions', subscription.id); + await this._updateSubscription(updatedSubscription); + } return true; } @@ -177,6 +178,40 @@ module.exports = class StripePaymentProcessor { }); } + async setComplimentarySubscription(member) { + const subscriptions = await this.getActiveSubscriptions(member); + const complimentaryPlan = this._plans.find(plan => (plan.nickname === 'Complimentary')); + + const customer = await this._customerForMemberCheckoutSession(member); + + if (!subscriptions.length) { + const subscription = await create(this._stripe, 'subscriptions', { + customer: customer.id, + items: [{ + plan: complimentaryPlan.id + }] + }); + + await this._updateSubscription(subscription); + } else { + // NOTE: we should only ever have 1 active subscription, but just in case there is more update is done on all of them + for (const subscription of subscriptions) { + const updatedSubscription = await update(this._stripe, 'subscriptions', subscription.id, { + proration_behavior: 'none', + plan: complimentaryPlan.id + }); + + await this._updateSubscription(updatedSubscription); + } + } + } + + async cancelComplimentarySubscription(member) { + // NOTE: a more explicit way would be cancelling just the "Complimentary" subscription, but doing it + // through existing method achieves the same as there should be only one subscription at a time + await this.cancelAllSubscriptions(member); + } + async getActiveSubscriptions(member) { const subscriptions = await this.getSubscriptions(member); diff --git a/ghost/members-api/lib/users.js b/ghost/members-api/lib/users.js index 5febeeba4a..e7f1e8cc73 100644 --- a/ghost/members-api/lib/users.js +++ b/ghost/members-api/lib/users.js @@ -75,6 +75,18 @@ module.exports = function ({ } } + async function setComplimentarySubscription(member) { + if (stripe) { + await stripe.setComplimentarySubscription(member); + } + } + + async function cancelComplimentarySubscription(member) { + if (stripe) { + await stripe.cancelComplimentarySubscription(member); + } + } + async function get(data, options) { debug(`get id:${data.id} email:${data.email}`); const member = await getMember(data, options); @@ -146,6 +158,8 @@ module.exports = function ({ get, destroy, getStripeSubscriptions, + setComplimentarySubscription, + cancelComplimentarySubscription, destroyStripeSubscriptions }; };