From c58e83c9d7aa26a5852223d58e0ff0185cf45cba Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 18 Oct 2021 15:27:17 +0200 Subject: [PATCH] Wired up OfferRedemption storage refs https://github.com/TryGhost/Team/issues/1132 We have to include the Offer on the metadata for the Stripe Checkout - as Offers with a duration of 'once' will not always be present on the Subscription after fetching it. Once we receive the Stripe Checkout webhook we emit an event for subscription created - the reason we use an event is because this logic should eventually live in a Payments/Stripe module - and we'd want to decouple it from the Members module. The Members module is in charge of writing Offer Redemptions - rather than the Offers module - because Offer Redemptions are "owned" by a Member - and merely reference and Offer. Eventually Offer Redemptions could be replaced by Subscriptions. --- ghost/member-events/index.js | 3 +- .../lib/SubscriptionCreatedEvent.js | 25 ++++++++++++++++ ghost/members-api/lib/MembersAPI.js | 2 ++ ghost/members-api/lib/controllers/router.js | 7 +++-- ghost/members-api/lib/repositories/member.js | 30 ++++++++++++++++--- .../lib/services/stripe-webhook.js | 12 ++++++++ ghost/members-api/package.json | 2 ++ 7 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 ghost/member-events/lib/SubscriptionCreatedEvent.js diff --git a/ghost/member-events/index.js b/ghost/member-events/index.js index 686268be89..0d1bc19bbc 100644 --- a/ghost/member-events/index.js +++ b/ghost/member-events/index.js @@ -3,5 +3,6 @@ module.exports = { MemberUnsubscribeEvent: require('./lib/MemberUnsubscribeEvent'), MemberSignupEvent: require('./lib/MemberSignupEvent'), MemberPaidConverstionEvent: require('./lib/MemberPaidConversionEvent'), - MemberPaidCancellationEvent: require('./lib/MemberPaidCancellationEvent') + MemberPaidCancellationEvent: require('./lib/MemberPaidCancellationEvent'), + SubscriptionCreatedEvent: require('./lib/SubscriptionCreatedEvent') }; diff --git a/ghost/member-events/lib/SubscriptionCreatedEvent.js b/ghost/member-events/lib/SubscriptionCreatedEvent.js new file mode 100644 index 0000000000..1ecf614452 --- /dev/null +++ b/ghost/member-events/lib/SubscriptionCreatedEvent.js @@ -0,0 +1,25 @@ +/** + * @typedef {object} SubscriptionCreatedEventData + * @prop {string} memberId + * @prop {string} subscriptionId + * @prop {string} offerId + */ + +module.exports = class SubscriptionCreatedEvent { + /** + * @param {SubscriptionCreatedEventData} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {SubscriptionCreatedEventData} data + * @param {Date} [timestamp] + */ + static create(data, timestamp) { + return new SubscriptionCreatedEvent(data, timestamp || new Date); + } +}; diff --git a/ghost/members-api/lib/MembersAPI.js b/ghost/members-api/lib/MembersAPI.js index f46a749164..00b66a3ba3 100644 --- a/ghost/members-api/lib/MembersAPI.js +++ b/ghost/members-api/lib/MembersAPI.js @@ -49,6 +49,7 @@ module.exports = function MembersAPI({ MemberProductEvent, MemberEmailChangeEvent, MemberAnalyticEvent, + OfferRedemption, StripeProduct, StripePrice, Product, @@ -102,6 +103,7 @@ module.exports = function MembersAPI({ MemberEmailChangeEvent, MemberStatusEvent, MemberProductEvent, + OfferRedemption, StripeCustomer, StripeCustomerSubscription }); diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index 9c7beb57c2..5d8a3dfabe 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -123,6 +123,7 @@ module.exports = class RouterController { let ghostPriceId = req.body.priceId; const identity = req.body.identity; const offerId = req.body.offerId; + const metadata = req.body.metadata; if (!ghostPriceId && !offerId) { res.writeHead(400); @@ -154,6 +155,8 @@ module.exports = class RouterController { coupon = { id: offer.stripe_coupon_id }; + + metadata.offer = offer.id; } catch (err) { res.writeHead(500); return res.end('Could not use Offer.'); @@ -193,7 +196,7 @@ module.exports = class RouterController { successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, customerEmail: req.body.customerEmail, - metadata: req.body.metadata + metadata: metadata }); const publicKey = this._stripeAPIService.getPublicKey(); @@ -237,7 +240,7 @@ module.exports = class RouterController { coupon, successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, - metadata: req.body.metadata + metadata: metadata }); const publicKey = this._stripeAPIService.getPublicKey(); diff --git a/ghost/members-api/lib/repositories/member.js b/ghost/members-api/lib/repositories/member.js index 690598dfe2..1e9c234383 100644 --- a/ghost/members-api/lib/repositories/member.js +++ b/ghost/members-api/lib/repositories/member.js @@ -1,6 +1,8 @@ const _ = require('lodash'); const errors = require('@tryghost/errors'); const tpl = require('@tryghost/tpl'); +const DomainEvents = require('@tryghost/domain-events'); +const {SubscriptionCreatedEvent} = require('@tryghost/member-events'); const ObjectId = require('bson-objectid'); const messages = { @@ -42,6 +44,7 @@ module.exports = class MemberRepository { MemberProductEvent, StripeCustomer, StripeCustomerSubscription, + OfferRedemption, stripeAPIService, productRepository, tokenService, @@ -59,6 +62,18 @@ module.exports = class MemberRepository { this._productRepository = productRepository; this.tokenService = tokenService; this._logging = logger; + + DomainEvents.subscribe(SubscriptionCreatedEvent, async function (event) { + if (!event.data.offerId) { + return; + } + + await OfferRedemption.add({ + member_id: event.data.memberId, + subscription_id: event.data.subscriptionId, + offer_id: event.data.offerId + }); + }); } isActiveSubscriptionStatus(status) { @@ -492,7 +507,15 @@ module.exports = class MemberRepository { } } - async linkSubscription(data, options) { + async getSubscriptionByStripeID(id, options) { + const subscription = await this._StripeCustomerSubscription.findOne({ + subscription_id: id + }, options); + + return subscription; + } + + async linkSubscription(data, options = {}) { if (!this._stripeAPIService.configured) { throw new errors.BadRequestError(tpl(messages.noStripeConnection, {action: 'link Stripe Subscription'})); } @@ -522,9 +545,8 @@ module.exports = class MemberRepository { } const paymentMethod = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null; - const model = await this._StripeCustomerSubscription.findOne({ - subscription_id: subscription.id - }, options); + const model = await this.getSubscriptionByStripeID(subscription.id, options); + const subscriptionPriceData = _.get(subscription, 'items.data[0].price'); let ghostProduct; try { diff --git a/ghost/members-api/lib/services/stripe-webhook.js b/ghost/members-api/lib/services/stripe-webhook.js index 3224c2f6af..055d913eda 100644 --- a/ghost/members-api/lib/services/stripe-webhook.js +++ b/ghost/members-api/lib/services/stripe-webhook.js @@ -1,5 +1,7 @@ const _ = require('lodash'); const errors = require('@tryghost/errors'); +const DomainEvents = require('@tryghost/domain-events'); +const {SubscriptionCreatedEvent} = require('@tryghost/member-events'); module.exports = class StripeWebhookService { /** @@ -274,6 +276,16 @@ module.exports = class StripeWebhookService { }); } + const subscription = await this._memberRepository.getSubscriptionByStripeID(session.subscription); + + const event = SubscriptionCreatedEvent.create({ + memberId: member.id, + subscriptionId: subscription.id, + offerId: session.metadata.offer || null + }); + + DomainEvents.dispatch(event); + if (checkoutType !== 'upgrade') { const emailType = 'signup'; this._sendEmailWithMagicLink({ diff --git a/ghost/members-api/package.json b/ghost/members-api/package.json index 80dd165c37..defd8817a1 100644 --- a/ghost/members-api/package.json +++ b/ghost/members-api/package.json @@ -27,10 +27,12 @@ }, "dependencies": { "@tryghost/debug": "^0.1.2", + "@tryghost/domain-events": "^0.1.2", "@tryghost/errors": "^0.2.9", "@tryghost/ignition-errors": "^0.1.2", "@tryghost/magic-link": "^1.0.13", "@tryghost/member-analytics-service": "^0.1.2", + "@tryghost/member-events": "^0.2.1", "@tryghost/members-analytics-ingress": "^0.1.3", "@tryghost/members-stripe-service": "^0.3.0", "@tryghost/tpl": "^0.1.2",