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(); });