From 1f300fb781f0227c14ca5b306f0318806388f5d1 Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Tue, 1 Nov 2022 21:47:49 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20checkout=20sessions=20wh?= =?UTF-8?q?en=20using=20Offers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/TryGhost/Team/issues/2195 The issue here is two-fold, and specific to using Offers so was not caught by any automated tests. First, we were incorrectly comparing the tier.id to the offer.tier.id - this is because the Tier objects id property is an instance of ObjectID rather than a string. Secondly we were passing through the cadence parameter from the request body, but when using Offers this is not including in the request, so we must pull the data off of the Offer object instead and pass that to the payments service. --- ...reate-stripe-checkout-session.test.js.snap | 16 +++++ .../create-stripe-checkout-session.test.js | 72 +++++++++++++++++++ ghost/members-api/lib/controllers/router.js | 3 +- ghost/payments/lib/payments.js | 2 +- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/ghost/core/test/e2e-api/members/__snapshots__/create-stripe-checkout-session.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/create-stripe-checkout-session.test.js.snap index a97adfbe0d..2f0d202b9f 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/create-stripe-checkout-session.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/create-stripe-checkout-session.test.js.snap @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Create Stripe Checkout Session Can create a checkout session when using offers 1: [body] 1`] = ` +Object { + "url": "https://site.com", +} +`; + +exports[`Create Stripe Checkout Session Can create a checkout session when using offers 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "*", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-type": "application/json", + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + exports[`Create Stripe Checkout Session Does allow to create a checkout session if the customerEmail is not associated with a paid member 1: [body] 1`] = ` Object { "url": "https://site.com", diff --git a/ghost/core/test/e2e-api/members/create-stripe-checkout-session.test.js b/ghost/core/test/e2e-api/members/create-stripe-checkout-session.test.js index c6da405beb..9ffc9eb6ba 100644 --- a/ghost/core/test/e2e-api/members/create-stripe-checkout-session.test.js +++ b/ghost/core/test/e2e-api/members/create-stripe-checkout-session.test.js @@ -55,6 +55,78 @@ describe('Create Stripe Checkout Session', function () { }); }); + it('Can create a checkout session when using offers', async function () { + const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price'); + const paidTier = tiers.find(tier => tier.type === 'paid'); + const {body: {offers: [offer]}} = await adminAgent.post('/offers/').body({ + offers: [{ + name: 'Test Offer', + code: 'test-offer', + cadence: 'month', + status: 'active', + currency: 'usd', + type: 'percent', + amount: 20, + duration: 'once', + duration_in_months: null, + display_title: 'Test Offer', + display_description: null, + tier: { + id: paidTier.id + } + }] + }); + + nock('https://api.stripe.com') + .persist() + .get(/v1\/.*/) + .reply((uri, body) => { + const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null]; + if (match) { + if (resource === 'products') { + return [200, { + id: id, + active: true + }]; + } + if (resource === 'prices') { + return [200, { + id: id, + active: true, + currency: 'usd', + unit_amount: 500 + }]; + } + } + + return [500]; + }); + + nock('https://api.stripe.com') + .persist() + .post(/v1\/.*/) + .reply((uri, body) => { + if (uri === '/v1/checkout/sessions') { + return [200, {id: 'cs_123', url: 'https://site.com'}]; + } + + if (uri === '/v1/coupons') { + return [200, {id: 'coupon_123', url: 'https://site.com'}]; + } + + return [500]; + }); + + await membersAgent.post('/api/create-stripe-checkout-session/') + .body({ + customerEmail: 'free@test.com', + offerId: offer.id + }) + .expectStatus(200) + .matchBodySnapshot() + .matchHeaderSnapshot(); + }); + it('Does allow to create a checkout session if the customerEmail is not associated with a paid member', async function () { const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price'); diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index 90b3ddd5fe..eb59efca36 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -142,7 +142,7 @@ module.exports = class RouterController { async createCheckoutSession(req, res) { let ghostPriceId = req.body.priceId; const tierId = req.body.tierId; - const cadence = req.body.cadence; + let cadence = req.body.cadence; const identity = req.body.identity; const offerId = req.body.offerId; const metadata = req.body.metadata ?? {}; @@ -185,6 +185,7 @@ module.exports = class RouterController { if (offerId) { offer = await this._offersAPI.getOffer({id: offerId}); tier = await this._tiersService.api.read(offer.tier.id); + cadence = offer.cadence; } else { offer = null; tier = await this._tiersService.api.read(tierId); diff --git a/ghost/payments/lib/payments.js b/ghost/payments/lib/payments.js index b050e5917f..1096dc61b2 100644 --- a/ghost/payments/lib/payments.js +++ b/ghost/payments/lib/payments.js @@ -67,7 +67,7 @@ class PaymentsService { let coupon = null; let trialDays = null; if (offer) { - if (offer.tier.id !== tier.id) { + if (!tier.id.equals(offer.tier.id)) { throw new BadRequestError({ message: 'This Offer is not valid for the Tier' });