From 3a27d1bd0cace90939bdf4d038613de7596a58da Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 4 May 2021 21:27:58 +0530 Subject: [PATCH] Updated APIs to use price ids refs https://github.com/TryGhost/Team/issues/637 All the APIs that currently work with price names needs to be updated to work with price ids instead to work with custom prices/products. This change updates APIs to work with Price IDs in `checkout` , `updateSubscription` and other APIs/methods. --- ghost/members-api/index.js | 11 +++--- .../lib/controllers/member/index.js | 34 +++++++++++-------- .../lib/controllers/router/index.js | 27 ++++++++------- .../lib/repositories/member/index.js | 19 +++++++---- .../lib/services/stripe-api/index.js | 20 ++++++----- .../unit/lib/controllers/member/index.test.js | 16 ++++++--- 6 files changed, 77 insertions(+), 50 deletions(-) diff --git a/ghost/members-api/index.js b/ghost/members-api/index.js index ef5638c273..0259173db8 100644 --- a/ghost/members-api/index.js +++ b/ghost/members-api/index.js @@ -46,7 +46,8 @@ module.exports = function MembersApi({ MemberEmailChangeEvent, StripeProduct, StripePrice, - Product + Product, + Settings }, logger }) { @@ -72,6 +73,7 @@ module.exports = function MembersApi({ StripeProduct, StripePrice, Product, + Settings, logger }); @@ -106,8 +108,7 @@ module.exports = function MembersApi({ MemberPaidSubscriptionEvent, MemberPaymentEvent, MemberStatusEvent, - MemberLoginEvent, - MemberEmailChangeEvent + MemberLoginEvent }); const stripeWebhookService = new StripeWebhookService({ @@ -138,17 +139,17 @@ module.exports = function MembersApi({ const memberController = new MemberController({ memberRepository, + StripePrice, stripeAPIService, - stripePlansService, tokenService }); const routerController = new RouterController({ memberRepository, + StripePrice, allowSelfSignup, magicLinkService, stripeAPIService, - stripePlansService, tokenService, sendEmailWithMagicLink, config: { diff --git a/ghost/members-api/lib/controllers/member/index.js b/ghost/members-api/lib/controllers/member/index.js index 8944e1ba77..95179a38e5 100644 --- a/ghost/members-api/lib/controllers/member/index.js +++ b/ghost/members-api/lib/controllers/member/index.js @@ -5,17 +5,20 @@ const errors = require('ghost-ignition').errors; * * @param {object} deps * @param {any} deps.memberRepository - * @param {any} deps.stripePlansService + * @param {any} deps.StripePrice + * @param {any} deps.stripeApiService * @param {any} deps.tokenService */ module.exports = class MemberController { constructor({ memberRepository, - stripePlansService, + StripePrice, + stripeAPIService, tokenService }) { this._memberRepository = memberRepository; - this._stripePlansService = stripePlansService; + this._StripePrice = StripePrice; + this._stripeApiService = stripeAPIService; this._tokenService = tokenService; } @@ -26,12 +29,11 @@ module.exports = class MemberController { const cancelAtPeriodEnd = req.body.cancel_at_period_end; const smartCancel = req.body.smart_cancel; const cancellationReason = req.body.cancellation_reason; - const planName = req.body.planName; - - if (cancelAtPeriodEnd === undefined && planName === undefined && smartCancel === undefined) { + const ghostPriceId = req.body.priceId; + if (cancelAtPeriodEnd === undefined && ghostPriceId === undefined && smartCancel === undefined) { throw new errors.BadRequestError({ message: 'Updating subscription failed!', - help: 'Request should contain "cancel_at_period_end" or "planName" or "smart_cancel" field.' + help: 'Request should contain "cancel_at_period_end" or "priceId" or "smart_cancel" field.' }); } @@ -70,18 +72,22 @@ module.exports = class MemberController { }); } - if (planName !== undefined) { - const plan = this._stripePlansService.getPlan(planName); - if (!plan) { - throw new errors.BadRequestError({ - message: 'Updating subscription failed! Could not find plan' - }); + if (ghostPriceId !== undefined) { + const price = await this._StripePrice.findOne({ + id: ghostPriceId + }); + + if (!price) { + res.writeHead(404); + return res.end('Not Found.'); } + + const priceId = price.get('stripe_price_id'); await this._memberRepository.updateSubscription({ email, subscription: { subscription_id: subscriptionId, - plan: plan.id + price: priceId } }); } else if (cancelAtPeriodEnd !== undefined) { diff --git a/ghost/members-api/lib/controllers/router/index.js b/ghost/members-api/lib/controllers/router/index.js index 5ca1228d80..dae9b6d684 100644 --- a/ghost/members-api/lib/controllers/router/index.js +++ b/ghost/members-api/lib/controllers/router/index.js @@ -7,29 +7,29 @@ const errors = require('ghost-ignition').errors; * * @param {object} deps * @param {any} deps.memberRepository + * @param {any} deps.StripePrice * @param {boolean} deps.allowSelfSignup * @param {any} deps.magicLinkService * @param {any} deps.stripeAPIService - * @param {any} deps.stripePlanService * @param {any} deps.tokenService * @param {any} deps.config */ module.exports = class RouterController { constructor({ memberRepository, + StripePrice, allowSelfSignup, magicLinkService, stripeAPIService, - stripePlansService, tokenService, sendEmailWithMagicLink, config }) { this._memberRepository = memberRepository; + this._StripePrice = StripePrice; this._allowSelfSignup = allowSelfSignup; this._magicLinkService = magicLinkService; this._stripeAPIService = stripeAPIService; - this._stripePlansService = stripePlansService; this._tokenService = tokenService; this._sendEmailWithMagicLink = sendEmailWithMagicLink; this._config = config; @@ -111,21 +111,24 @@ module.exports = class RouterController { } async createCheckoutSession(req, res) { - const planName = req.body.plan; + const ghostPriceId = req.body.priceId; const identity = req.body.identity; - if (!planName) { + if (!ghostPriceId) { res.writeHead(400); return res.end('Bad Request.'); } - // NOTE: never allow "Complimentary" plan to be subscribed to from the client - if (planName.toLowerCase() === 'complimentary') { - res.writeHead(400); - return res.end('Bad Request.'); + const price = await this._StripePrice.findOne({ + id: ghostPriceId + }); + + if (!price) { + res.writeHead(404); + return res.end('Not Found.'); } - const plan = this._stripePlansService.getPlan(planName); + const priceId = price.get('stripe_price_id'); let email; try { @@ -144,7 +147,7 @@ module.exports = class RouterController { if (!member) { const customer = null; - const session = await this._stripeAPIService.createCheckoutSession(plan, customer, { + const session = await this._stripeAPIService.createCheckoutSession(priceId, customer, { successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, customerEmail: req.body.customerEmail, @@ -190,7 +193,7 @@ module.exports = class RouterController { } try { - const session = await this._stripeAPIService.createCheckoutSession(plan, stripeCustomer, { + const session = await this._stripeAPIService.createCheckoutSession(priceId, stripeCustomer, { successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, metadata: req.body.metadata diff --git a/ghost/members-api/lib/repositories/member/index.js b/ghost/members-api/lib/repositories/member/index.js index 59c6d3b5ab..86b73dc109 100644 --- a/ghost/members-api/lib/repositories/member/index.js +++ b/ghost/members-api/lib/repositories/member/index.js @@ -511,21 +511,28 @@ module.exports = class MemberRepository { const member = await this._Member.findOne(findQuery); - const subscription = await member.related('stripeSubscriptions').query({ + const subscriptionModel = await member.related('stripeSubscriptions').query({ where: { subscription_id: data.subscription.subscription_id } }).fetchOne(options); - if (!subscription) { + if (!subscriptionModel) { throw new Error('Subscription not found'); } let updatedSubscription; - if (data.subscription.plan) { - updatedSubscription = await this._stripeAPIService.changeSubscriptionPlan( - data.subscription.subscription_id, - data.subscription.plan + if (data.subscription.price) { + const subscription = await this._stripeAPIService.getSubscription( + data.subscription.subscription_id + ); + + const subscriptionItem = subscription.items.data[0]; + + updatedSubscription = await this._stripeAPIService.updateSubscriptionItemPrice( + subscription.id, + subscriptionItem.id, + data.subscription.price ); } diff --git a/ghost/members-api/lib/services/stripe-api/index.js b/ghost/members-api/lib/services/stripe-api/index.js index 0db414085e..985ec9ed98 100644 --- a/ghost/members-api/lib/services/stripe-api/index.js +++ b/ghost/members-api/lib/services/stripe-api/index.js @@ -383,13 +383,13 @@ module.exports = class StripeAPIService { } /** - * @param {IPlan} plan + * @param {string} priceId * @param {ICustomer} customer * @param {object} options * * @returns {Promise} */ - async createCheckoutSession(plan, customer, options) { + async createCheckoutSession(priceId, customer, options) { const metadata = options.metadata || undefined; const customerEmail = customer ? customer.email : options.customerEmail; await this._rateLimitBucket.throttle(); @@ -404,7 +404,7 @@ module.exports = class StripeAPIService { subscription_data: { trial_from_plan: true, items: [{ - plan: plan.id + plan: priceId }] } }); @@ -543,15 +543,19 @@ module.exports = class StripeAPIService { } /** - * @param {string} id - The ID of the Subscription to modify - * @param {string} plan - The ID of the new Plan + * @param {string} subscriptionId - The ID of the Subscription to modify + * @param {string} id - The ID of the SubscriptionItem + * @param {string} price - The ID of the new Price * * @returns {Promise} */ - async changeSubscriptionPlan(id, plan) { + async updateSubscriptionItemPrice(subscriptionId, id, price) { await this._rateLimitBucket.throttle(); - const subscription = await this._stripe.subscriptions.update(id, { - plan, + const subscription = await this._stripe.subscriptions.update(subscriptionId, { + items: [{ + id, + price + }], cancel_at_period_end: false, metadata: { cancellation_reason: null diff --git a/ghost/members-api/test/unit/lib/controllers/member/index.test.js b/ghost/members-api/test/unit/lib/controllers/member/index.test.js index 52711ad277..a0f9eb0728 100644 --- a/ghost/members-api/test/unit/lib/controllers/member/index.test.js +++ b/ghost/members-api/test/unit/lib/controllers/member/index.test.js @@ -7,8 +7,14 @@ describe('MemberController', function () { const tokenService = { decodeToken: sinon.fake.resolves({sub: 'fake@email.com'}) }; - const stripePlansService = { - getPlan: sinon.fake.returns({id: 'plan_id'}) + const StripePrice = { + findOne: sinon.fake.returns({ + id: 'plan_id', + stripe_price_id: 'stripe_price_id', + get: () => { + return 'stripe_price_id'; + } + }) }; const memberRepository = { @@ -16,21 +22,21 @@ describe('MemberController', function () { email: 'fake@email.com', subscription: { subscription_id: 'subscription_id', - plan: 'plan_id' + price: 'stripe_price_id' } }) }; const controller = new MemberController({ memberRepository, - stripePlansService, + StripePrice, tokenService }); const req = { body: { identity: 'token', - planName: 'plan_name' + priceId: 'plan_name' }, params: { id: 'subscription_id'