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'),
MemberSignupEvent: require('./lib/MemberSignupEvent'),
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,
MemberEmailChangeEvent,
MemberAnalyticEvent,
OfferRedemption,
StripeProduct,
StripePrice,
Product,
@ -102,6 +103,7 @@ module.exports = function MembersAPI({
MemberEmailChangeEvent,
MemberStatusEvent,
MemberProductEvent,
OfferRedemption,
StripeCustomer,
StripeCustomerSubscription
});

View File

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

View File

@ -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 {

View File

@ -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({

View File

@ -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",