From 8bdad78377d50fd9a53a761e50a7a4ef9de5847b Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Wed, 7 Dec 2022 14:30:11 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20broken=20redemption=20co?= =?UTF-8?q?unt=20for=20offers=20(#15954)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://github.com/TryGhost/Team/issues/2369 - offer id was not getting attached to stripe checkout metadata, causing the checkout event to not store any offer information for a subscription. This got changed in a prev refactor [here](https://github.com/TryGhost/Ghost/commit/25d8d694a051781e9f45e3542b75143462c69ec4#diff-b7dfcd660902a2a20dff7da5e886d8e10234bda4ba78228255afc8d4a8e78cf6L206) - cleans up offer id handling for checkout session event --- ghost/members-api/lib/controllers/router.js | 3 + ghost/members-api/lib/repositories/member.js | 2 +- .../test/unit/lib/controllers/router.test.js | 93 +++++++++++++++++++ .../test/unit/lib/repositories/member.test.js | 43 +++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 ghost/members-api/test/unit/lib/controllers/router.test.js diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index eb59efca36..3b133f9636 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -186,6 +186,9 @@ module.exports = class RouterController { offer = await this._offersAPI.getOffer({id: offerId}); tier = await this._tiersService.api.read(offer.tier.id); cadence = offer.cadence; + // Attach offer information to stripe metadata for free trial offers + // free trial offers don't have associated stripe coupons + metadata.offer = offer.id; } else { offer = null; tier = await this._tiersService.api.read(tierId); diff --git a/ghost/members-api/lib/repositories/member.js b/ghost/members-api/lib/repositories/member.js index ba266fb3e2..ea2fab6810 100644 --- a/ghost/members-api/lib/repositories/member.js +++ b/ghost/members-api/lib/repositories/member.js @@ -1033,7 +1033,7 @@ module.exports = class MemberRepository { tierId: ghostProduct?.get('id'), memberId: member.id, subscriptionId: subscriptionModel.get('id'), - offerId: data.offerId, + offerId: offerId, attribution: data.attribution, batchId: options.batch_id }); diff --git a/ghost/members-api/test/unit/lib/controllers/router.test.js b/ghost/members-api/test/unit/lib/controllers/router.test.js new file mode 100644 index 0000000000..c968046b62 --- /dev/null +++ b/ghost/members-api/test/unit/lib/controllers/router.test.js @@ -0,0 +1,93 @@ +const sinon = require('sinon'); +const RouterController = require('../../../../lib/controllers/router'); + +describe('RouterController', function () { + describe('createCheckoutSession', function (){ + let offersAPI; + let paymentsService; + let tiersService; + let stripeAPIService; + let labsService; + let getPaymentLinkSpy; + + beforeEach(async function () { + getPaymentLinkSpy = sinon.spy(); + + tiersService = { + api: { + read: sinon.stub().resolves({ + id: 'tier_123' + }) + } + }; + + paymentsService = { + getPaymentLink: getPaymentLinkSpy + }; + + offersAPI = { + getOffer: sinon.stub().resolves({ + id: 'offer_123', + tier: { + id: 'tier_123' + } + }), + findOne: sinon.stub().resolves({ + related: () => { + return { + query: sinon.stub().returns({ + fetchOne: sinon.stub().resolves({}) + }), + toJSON: sinon.stub().returns([]), + fetch: sinon.stub().resolves({ + toJSON: sinon.stub().returns({}) + }) + }; + }, + toJSON: sinon.stub().returns({}) + }), + edit: sinon.stub().resolves({ + attributes: {}, + _previousAttributes: {} + }) + }; + + stripeAPIService = { + configured: true + }; + labsService = { + isSet: sinon.stub().returns(true) + }; + }); + + it('passes offer metadata to payment link method', async function (){ + const routerController = new RouterController({ + tiersService, + paymentsService, + offersAPI, + stripeAPIService, + labsService + }); + + await routerController.createCheckoutSession({ + body: { + offerId: 'offer_123' + } + }, { + writeHead: () => {}, + end: () => {} + }); + + getPaymentLinkSpy.calledOnce.should.be.true(); + + // Payment link is called with the offer id in metadata + getPaymentLinkSpy.calledWith(sinon.match({ + metadata: {offer: 'offer_123'} + })).should.be.true(); + }); + + afterEach(function () { + sinon.restore(); + }); + }); +}); diff --git a/ghost/members-api/test/unit/lib/repositories/member.test.js b/ghost/members-api/test/unit/lib/repositories/member.test.js index 8b7d02bb61..3fd632f656 100644 --- a/ghost/members-api/test/unit/lib/repositories/member.test.js +++ b/ghost/members-api/test/unit/lib/repositories/member.test.js @@ -141,6 +141,7 @@ describe('MemberRepository', function () { let MemberProductEvent; let stripeAPIService; let productRepository; + let offerRepository; let labsService; let subscriptionData; @@ -221,6 +222,12 @@ describe('MemberRepository', function () { labsService = { isSet: sinon.stub().returns(true) }; + + offerRepository = { + getById: sinon.stub().resolves({ + id: 'offer_123' + }) + }; }); it('dispatches paid subscription event', async function (){ @@ -250,6 +257,42 @@ describe('MemberRepository', function () { notifySpy.calledOnce.should.be.true(); }); + it('attaches offer information to subscription event', async function (){ + const repo = new MemberRepository({ + stripeAPIService, + StripeCustomerSubscription, + MemberPaidSubscriptionEvent, + MemberProductEvent, + productRepository, + offerRepository, + labsService, + Member + }); + + sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null); + + DomainEvents.subscribe(SubscriptionCreatedEvent, notifySpy); + + await repo.linkSubscription({ + id: 'member_id_123', + subscription: subscriptionData, + offerId: 'offer_123' + }, { + transacting: { + executionPromise: Promise.resolve() + }, + context: {} + }); + + notifySpy.calledOnce.should.be.true(); + notifySpy.calledWith(sinon.match((event) => { + if (event.data.offerId === 'offer_123') { + return true; + } + return false; + })).should.be.true(); + }); + afterEach(function () { sinon.restore(); });