0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

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.
This commit is contained in:
Fabien O'Carroll 2021-10-18 15:27:17 +02:00
parent 05619a193c
commit c58e83c9d7
7 changed files with 74 additions and 7 deletions

View file

@ -3,5 +3,6 @@ module.exports = {
MemberUnsubscribeEvent: require('./lib/MemberUnsubscribeEvent'), MemberUnsubscribeEvent: require('./lib/MemberUnsubscribeEvent'),
MemberSignupEvent: require('./lib/MemberSignupEvent'), MemberSignupEvent: require('./lib/MemberSignupEvent'),
MemberPaidConverstionEvent: require('./lib/MemberPaidConversionEvent'), MemberPaidConverstionEvent: require('./lib/MemberPaidConversionEvent'),
MemberPaidCancellationEvent: require('./lib/MemberPaidCancellationEvent') MemberPaidCancellationEvent: require('./lib/MemberPaidCancellationEvent'),
SubscriptionCreatedEvent: require('./lib/SubscriptionCreatedEvent')
}; };

View file

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

View file

@ -49,6 +49,7 @@ module.exports = function MembersAPI({
MemberProductEvent, MemberProductEvent,
MemberEmailChangeEvent, MemberEmailChangeEvent,
MemberAnalyticEvent, MemberAnalyticEvent,
OfferRedemption,
StripeProduct, StripeProduct,
StripePrice, StripePrice,
Product, Product,
@ -102,6 +103,7 @@ module.exports = function MembersAPI({
MemberEmailChangeEvent, MemberEmailChangeEvent,
MemberStatusEvent, MemberStatusEvent,
MemberProductEvent, MemberProductEvent,
OfferRedemption,
StripeCustomer, StripeCustomer,
StripeCustomerSubscription StripeCustomerSubscription
}); });

View file

@ -123,6 +123,7 @@ module.exports = class RouterController {
let ghostPriceId = req.body.priceId; let ghostPriceId = req.body.priceId;
const identity = req.body.identity; const identity = req.body.identity;
const offerId = req.body.offerId; const offerId = req.body.offerId;
const metadata = req.body.metadata;
if (!ghostPriceId && !offerId) { if (!ghostPriceId && !offerId) {
res.writeHead(400); res.writeHead(400);
@ -154,6 +155,8 @@ module.exports = class RouterController {
coupon = { coupon = {
id: offer.stripe_coupon_id id: offer.stripe_coupon_id
}; };
metadata.offer = offer.id;
} catch (err) { } catch (err) {
res.writeHead(500); res.writeHead(500);
return res.end('Could not use Offer.'); return res.end('Could not use Offer.');
@ -193,7 +196,7 @@ module.exports = class RouterController {
successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, successUrl: req.body.successUrl || this._config.checkoutSuccessUrl,
cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl,
customerEmail: req.body.customerEmail, customerEmail: req.body.customerEmail,
metadata: req.body.metadata metadata: metadata
}); });
const publicKey = this._stripeAPIService.getPublicKey(); const publicKey = this._stripeAPIService.getPublicKey();
@ -237,7 +240,7 @@ module.exports = class RouterController {
coupon, coupon,
successUrl: req.body.successUrl || this._config.checkoutSuccessUrl, successUrl: req.body.successUrl || this._config.checkoutSuccessUrl,
cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl, cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl,
metadata: req.body.metadata metadata: metadata
}); });
const publicKey = this._stripeAPIService.getPublicKey(); const publicKey = this._stripeAPIService.getPublicKey();

View file

@ -1,6 +1,8 @@
const _ = require('lodash'); const _ = require('lodash');
const errors = require('@tryghost/errors'); const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl'); const tpl = require('@tryghost/tpl');
const DomainEvents = require('@tryghost/domain-events');
const {SubscriptionCreatedEvent} = require('@tryghost/member-events');
const ObjectId = require('bson-objectid'); const ObjectId = require('bson-objectid');
const messages = { const messages = {
@ -42,6 +44,7 @@ module.exports = class MemberRepository {
MemberProductEvent, MemberProductEvent,
StripeCustomer, StripeCustomer,
StripeCustomerSubscription, StripeCustomerSubscription,
OfferRedemption,
stripeAPIService, stripeAPIService,
productRepository, productRepository,
tokenService, tokenService,
@ -59,6 +62,18 @@ module.exports = class MemberRepository {
this._productRepository = productRepository; this._productRepository = productRepository;
this.tokenService = tokenService; this.tokenService = tokenService;
this._logging = logger; 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) { 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) { if (!this._stripeAPIService.configured) {
throw new errors.BadRequestError(tpl(messages.noStripeConnection, {action: 'link Stripe Subscription'})); 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 paymentMethod = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
const model = await this._StripeCustomerSubscription.findOne({ const model = await this.getSubscriptionByStripeID(subscription.id, options);
subscription_id: subscription.id
}, options);
const subscriptionPriceData = _.get(subscription, 'items.data[0].price'); const subscriptionPriceData = _.get(subscription, 'items.data[0].price');
let ghostProduct; let ghostProduct;
try { try {

View file

@ -1,5 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const errors = require('@tryghost/errors'); const errors = require('@tryghost/errors');
const DomainEvents = require('@tryghost/domain-events');
const {SubscriptionCreatedEvent} = require('@tryghost/member-events');
module.exports = class StripeWebhookService { 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') { if (checkoutType !== 'upgrade') {
const emailType = 'signup'; const emailType = 'signup';
this._sendEmailWithMagicLink({ this._sendEmailWithMagicLink({

View file

@ -27,10 +27,12 @@
}, },
"dependencies": { "dependencies": {
"@tryghost/debug": "^0.1.2", "@tryghost/debug": "^0.1.2",
"@tryghost/domain-events": "^0.1.2",
"@tryghost/errors": "^0.2.9", "@tryghost/errors": "^0.2.9",
"@tryghost/ignition-errors": "^0.1.2", "@tryghost/ignition-errors": "^0.1.2",
"@tryghost/magic-link": "^1.0.13", "@tryghost/magic-link": "^1.0.13",
"@tryghost/member-analytics-service": "^0.1.2", "@tryghost/member-analytics-service": "^0.1.2",
"@tryghost/member-events": "^0.2.1",
"@tryghost/members-analytics-ingress": "^0.1.3", "@tryghost/members-analytics-ingress": "^0.1.3",
"@tryghost/members-stripe-service": "^0.3.0", "@tryghost/members-stripe-service": "^0.3.0",
"@tryghost/tpl": "^0.1.2", "@tryghost/tpl": "^0.1.2",