0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-04 02:01:58 -05:00

Improved docstrings in the Stripe service and added README.md (#22214)

no issue

- No real code changes
- Added README.md to stripe service as a starting point to document some of the more complex stripe related flows
- Added typedefs and docstrings to most of the Stripe service code for improved type safety and clarity
This commit is contained in:
Chris Raible 2025-02-18 11:16:40 -08:00 committed by GitHub
parent 5f465b14e6
commit bc25569843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 333 additions and 41 deletions

63
ghost/stripe/README.md Normal file
View file

@ -0,0 +1,63 @@
# Stripe Service
This package contains code for Ghost's Stripe integration. It interacts with Stripe's API and handles webhooks.
The main export of this package is the `StripeService` class. It includes a wrapper around the Stripe API and webhook handling logic. It is instantiated in Ghost's `core/server/services/stripe` service.
## Stripe API
The `StripeAPI` class is a wrapper around the Stripe API. It is used by the `StripeService` class to interact with Stripe's API.
## Stripe Webhooks
Ghost listens for Stripe webhooks to know when a customer has subscribed to a plan, when a subscription has been cancelled, when a payment has failed, etc.
Things to keep in mind when working with Stripe webhooks:
- Webhooks can arrive out of order. `checkout.session.completed` webhooks may arrive before or after `customer.subscription.created` webhooks.
- Webhooks can be received and processed in parallel, so you should not rely on the order of the webhooks to determine the order of operations.
- Operations in Stripe almost always produce multiple events, increasing the likelihood of race conditions.
See Stripe's [Webhooks Guide](https://docs.stripe.com/webhooks) for more information.
### Webhook Manager
This class is responsible for registering the webhook endpoints with Stripe, so Stripe knows where to send the webhooks.
### Webhook Controller
This class is responsible for handling the webhook events. It accepts the webhook event payload and delegates it to the appropriate handler based on the event type.
### Events
The Webhook Controller listens for the following events:
- `customer.subscription.deleted`
- `customer.subscription.updated`
- `customer.subscription.created`
- `invoice.payment_succeeded`
- `checkout.session.completed`
## Stripe Flows
### Checkout Session Flow: New Subscription
```mermaid
sequenceDiagram
actor Member as Member
participant Portal
participant Ghost
participant Stripe
Member->>Portal: Signs up for a paid plan
Portal->>Ghost: Create checkout session
Ghost->>Stripe: Create checkout session
Stripe-->>Ghost: Return session ID
Ghost-->>Portal: Return session ID
Portal->>Stripe: Redirect to checkout page
Note over Portal: Member enters payment details in Stripe's secure portal
Stripe-->>Portal: Redirect to success URL
par Webhook Events
Stripe->>Ghost: customer.subscription.created
Ghost->>Ghost: Upsert member and subscription
Stripe->>Ghost: checkout.session.completed
Ghost->>Ghost: Upsert member and subscription
Stripe->>Ghost: customer.subscription.updated
Ghost->>Ghost: Upsert member and subscription
Stripe->>Ghost: invoice.payment_succeeded
Ghost->>Ghost: Record payment
end
```

View file

@ -28,6 +28,15 @@ const STRIPE_API_VERSION = '2020-08-27';
* @typedef {import('stripe').Stripe.Plan} IPlan
* @typedef {import('stripe').Stripe.Price} IPrice
* @typedef {import('stripe').Stripe.WebhookEndpoint} IWebhookEndpoint
* @typedef {import('stripe').Stripe.Coupon} ICoupon
* @typedef {import('stripe').Stripe.CouponCreateParams} ICouponCreateParams
* @typedef {import('stripe').Stripe.ProductCreateParams} IProductCreateParams
* @typedef {import('stripe').Stripe.CustomerRetrieveParams} ICustomerRetrieveParams
* @typedef {import('stripe').Stripe.Checkout.Session} ICheckoutSession
* @typedef {import('stripe').Stripe.Checkout.SessionCreateParams} ICheckoutSessionCreateParams
* @typedef {import('stripe').Stripe.SubscriptionRetrieveParams} ISubscriptionRetrieveParams
* @typedef {import('stripe').Stripe.Subscription} ISubscription
* @typedef {import('stripe').Stripe.Checkout.SessionCreateParams.PaymentMethodType} IPaymentMethodType
*/
/**
@ -40,12 +49,14 @@ const STRIPE_API_VERSION = '2020-08-27';
* @prop {string} checkoutSessionCancelUrl
* @prop {string} checkoutSetupSessionSuccessUrl
* @prop {string} checkoutSetupSessionCancelUrl
* @prop {boolean} param.testEnv - indicates if the module is run in test environment (note, NOT the test mode)
* @prop {boolean} testEnv - indicates if the module is run in test environment (note, NOT the test mode)
*/
module.exports = class StripeAPI {
/**
* StripeAPI
* @param {object} deps
* @param {object} deps.labs
*/
constructor(deps) {
/** @type {Stripe} */
@ -54,6 +65,9 @@ module.exports = class StripeAPI {
this.labs = deps.labs;
}
/**
* @returns {IPaymentMethodType[]|undefined}
*/
get PAYMENT_METHOD_TYPES() {
if (this.labs.isSet('additionalPaymentMethods')) {
return undefined;
@ -62,20 +76,41 @@ module.exports = class StripeAPI {
}
}
/**
* Returns true if the Stripe API is configured.
* @returns {boolean}
*/
get configured() {
return this._configured;
}
/**
* Returns true if this package is running in a test environment (i.e. browser tests).
*
* Note: This is not the same as the Stripe API's test mode.
* @returns {boolean}
*/
get testEnv() {
return this._config.testEnv;
}
/**
* Returns the Stripe API mode (test or live).
*
* @returns {string}
*/
get mode() {
return this._testMode ? 'test' : 'live';
}
/**
* Configure the Stripe API.
* - Instantiates the Stripe API client
* - Sets the Stripe API mode
* - Configures rate limiting buckets
*
* @param {IStripeAPIConfig} config
*
* @returns {void}
*/
configure(config) {
@ -103,7 +138,11 @@ module.exports = class StripeAPI {
}
/**
* @param {object} options
* Create a new Stripe Coupon.
*
* @param {ICouponCreateParams} options
*
* @returns {Promise<ICoupon>}
*/
async createCoupon(options) {
await this._rateLimitBucket.throttle();
@ -113,6 +152,7 @@ module.exports = class StripeAPI {
}
/**
* Retrieve the Stripe Product object by ID.
* @param {string} id
*
* @returns {Promise<IProduct>}
@ -125,8 +165,8 @@ module.exports = class StripeAPI {
}
/**
* @param {object} options
* @param {string} options.name
* Create a new Stripe Product.
* @param {IProductCreateParams} options
*
* @returns {Promise<IProduct>}
*/
@ -138,6 +178,8 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Price.
*
* @param {object} options
* @param {string} options.product
* @param {boolean} options.active
@ -169,6 +211,8 @@ module.exports = class StripeAPI {
}
/**
* Update the Stripe Price object by ID.
*
* @param {string} id
* @param {object} options
* @param {boolean} [options.active]
@ -187,6 +231,8 @@ module.exports = class StripeAPI {
}
/**
* Update the Stripe Product object by ID.
*
* @param {string} id
* @param {object} options
* @param {string} options.name
@ -203,10 +249,13 @@ module.exports = class StripeAPI {
}
/**
* Retrieve the Stripe Customer object by ID.
*
* @param {string} id
* @param {import('stripe').Stripe.CustomerRetrieveParams} options
* @param {ICustomerRetrieveParams} options
*
* @returns {Promise<ICustomer|IDeletedCustomer>}
* @throws {Error}
*/
async getCustomer(id, options = {}) {
debug(`getCustomer(${id}, ${JSON.stringify(options)})`);
@ -227,6 +276,8 @@ module.exports = class StripeAPI {
}
/**
* Finds or creates a Stripe Customer for a Member.
*
* @deprecated
* @param {any} member
*
@ -309,6 +360,8 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Customer.
*
* @param {import('stripe').Stripe.CustomerCreateParams} options
*
* @returns {Promise<ICustomer>}
@ -327,6 +380,8 @@ module.exports = class StripeAPI {
}
/**
* Update the email address for a Stripe Customer.
*
* @param {string} id
* @param {string} email
*
@ -346,7 +401,7 @@ module.exports = class StripeAPI {
}
/**
* createWebhook.
* Create a new Stripe Webhook Endpoint.
*
* @param {string} url
* @param {import('stripe').Stripe.WebhookEndpointUpdateParams.EnabledEvent[]} events
@ -371,6 +426,8 @@ module.exports = class StripeAPI {
}
/**
* Delete a Stripe Webhook Endpoint by ID.
*
* @param {string} id
*
* @returns {Promise<void>}
@ -389,6 +446,8 @@ module.exports = class StripeAPI {
}
/**
* Update a Stripe Webhook Endpoint by ID and URL.
*
* @param {string} id
* @param {string} url
* @param {import('stripe').Stripe.WebhookEndpointUpdateParams.EnabledEvent[]} events
@ -415,7 +474,7 @@ module.exports = class StripeAPI {
}
/**
* parseWebhook.
* Parse a Stripe Webhook event.
*
* @param {string} body
* @param {string} signature
@ -436,6 +495,8 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Checkout Session for a new subscription.
*
* @param {string} priceId
* @param {ICustomer} customer
*
@ -447,7 +508,7 @@ module.exports = class StripeAPI {
* @param {number} options.trialDays
* @param {string} [options.coupon]
*
* @returns {Promise<import('stripe').Stripe.Checkout.Session>}
* @returns {Promise<ICheckoutSession>}
*/
async createCheckoutSession(priceId, customer, options) {
const metadata = options.metadata || undefined;
@ -518,14 +579,17 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Checkout Session for a donation.
*
* @param {object} options
* @param {Object.<String, any>} options.metadata
* @param {string} options.priceId
* @param {string} options.successUrl
* @param {string} options.cancelUrl
* @param {string} [options.customer]
* @param {Object.<String, any>} options.metadata
* @param {ICustomer} [options.customer]
* @param {string} [options.customerEmail]
*
* @returns {Promise<import('stripe').Stripe.Checkout.Session>}
* @returns {Promise<ICheckoutSession>}
*/
async createDonationCheckoutSession({priceId, successUrl, cancelUrl, metadata, customer, customerEmail}) {
await this._rateLimitBucket.throttle();
@ -589,12 +653,14 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Checkout Setup Session.
*
* @param {ICustomer} customer
* @param {object} options
* @param {string} options.successUrl
* @param {string} options.cancelUrl
* @param {string} options.currency - 3-letter ISO code in lowercase, e.g. `usd`
* @returns {Promise<import('stripe').Stripe.Checkout.Session>}
* @returns {Promise<ICheckoutSession>}
*/
async createCheckoutSetupSession(customer, options) {
await this._rateLimitBucket.throttle();
@ -612,23 +678,29 @@ module.exports = class StripeAPI {
// Note: this is required for dynamic payment methods
// https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-currency
// @ts-ignore
currency: this.labs.isSet('additionalPaymentMethods') ? options.currency : undefined
});
return session;
}
/**
* Get the Stripe public key.
*
* @returns {string}
*/
getPublicKey() {
return this._config.publicKey;
}
/**
* getPrice
* Retrieve the Stripe Price object by ID.
*
* @param {string} id
* @param {object} options
*
* @returns {Promise<import('stripe').Stripe.Price>}
* @returns {Promise<IPrice>}
*/
async getPrice(id, options = {}) {
debug(`getPrice(${id}, ${JSON.stringify(options)})`);
@ -637,12 +709,12 @@ module.exports = class StripeAPI {
}
/**
* getSubscription.
* Retrieve the Stripe Subscription object by ID.
*
* @param {string} id
* @param {import('stripe').Stripe.SubscriptionRetrieveParams} options
* @param {ISubscriptionRetrieveParams} options
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async getSubscription(id, options = {}) {
debug(`getSubscription(${id}, ${JSON.stringify(options)})`);
@ -658,11 +730,11 @@ module.exports = class StripeAPI {
}
/**
* cancelSubscription.
* Cancel the Stripe Subscription by ID.
*
* @param {string} id
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async cancelSubscription(id) {
debug(`cancelSubscription(${id})`);
@ -678,10 +750,12 @@ module.exports = class StripeAPI {
}
/**
* Cancel the Stripe Subscription at the end of the current period by ID.
*
* @param {string} id - The ID of the Subscription to modify
* @param {string} [reason=''] - The user defined cancellation reason
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async cancelSubscriptionAtPeriodEnd(id, reason = '') {
await this._rateLimitBucket.throttle();
@ -695,9 +769,11 @@ module.exports = class StripeAPI {
}
/**
* Continue the Stripe Subscription at the end of the current period by ID.
*
* @param {string} id - The ID of the Subscription to modify
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async continueSubscriptionAtPeriodEnd(id) {
await this._rateLimitBucket.throttle();
@ -711,9 +787,11 @@ module.exports = class StripeAPI {
}
/**
* Remove the coupon from the Stripe Subscription by ID.
*
* @param {string} id - The ID of the subscription to remove coupon from
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async removeCouponFromSubscription(id) {
await this._rateLimitBucket.throttle();
@ -724,6 +802,9 @@ module.exports = class StripeAPI {
}
/**
* Update the price of the Stripe SubscriptionItem by Subscription ID,
* SubscriptionItem ID, and Price ID.
*
* @param {string} subscriptionId - The ID of the Subscription to modify
* @param {string} id - The ID of the SubscriptionItem
* @param {string} price - The ID of the new Price
@ -731,7 +812,7 @@ module.exports = class StripeAPI {
* @param {('always_invoice'|'create_prorations'|'none')} [options.prorationBehavior='always_invoice'] - The proration behavior to use. See [Stripe docs](https://docs.stripe.com/api/subscriptions/update#update_subscription-proration_behavior) for more info
* @param {string} [options.cancellationReason=null] - The user defined cancellation reason
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async updateSubscriptionItemPrice(subscriptionId, id, price, options = {}) {
await this._rateLimitBucket.throttle();
@ -750,10 +831,12 @@ module.exports = class StripeAPI {
}
/**
* Create a new Stripe Subscription for a Customer by ID and Price ID.
*
* @param {string} customer - The ID of the Customer to create the subscription for
* @param {string} price - The ID of the new Price
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async createSubscription(customer, price) {
await this._rateLimitBucket.throttle();
@ -765,6 +848,8 @@ module.exports = class StripeAPI {
}
/**
* Retrieve the Stripe SetupIntent object by ID.
*
* @param {string} id
* @param {import('stripe').Stripe.SetupIntentRetrieveParams} options
*
@ -776,6 +861,8 @@ module.exports = class StripeAPI {
}
/**
* Attach a PaymentMethod to a Customer
*
* @param {string} customer
* @param {string} paymentMethod
*
@ -788,6 +875,8 @@ module.exports = class StripeAPI {
}
/**
* Retrieve the Stripe PaymentMethod object by ID.
*
* @param {string} id
*
* @returns {Promise<import('stripe').Stripe.PaymentMethod|null>}
@ -803,10 +892,12 @@ module.exports = class StripeAPI {
}
/**
* Update the default PaymentMethod for a Subscription.
*
* @param {string} subscription
* @param {string} paymentMethod
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async updateSubscriptionDefaultPaymentMethod(subscription, paymentMethod) {
await this._rateLimitBucket.throttle();
@ -816,9 +907,11 @@ module.exports = class StripeAPI {
}
/**
* Cancel the trial for a Stripe Subscription by ID.
*
* @param {string} id - The ID of the subscription to cancel the trial for
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
* @returns {Promise<ISubscription>}
*/
async cancelSubscriptionTrial(id) {
await this._rateLimitBucket.throttle();

View file

@ -8,7 +8,42 @@ const SubscriptionEventService = require('./services/webhook/SubscriptionEventSe
const InvoiceEventService = require('./services/webhook/InvoiceEventService');
const CheckoutSessionEventService = require('./services/webhook/CheckoutSessionEventService');
/**
* @typedef {object} IStripeServiceConfig
* @prop {string} secretKey The Stripe secret key
* @prop {string} publicKey The Stripe publishable key
* @prop {boolean} enablePromoCodes Whether to enable promo codes
* @prop {boolean} enableAutomaticTax Whether to enable automatic tax
* @prop {string} checkoutSessionSuccessUrl The URL to redirect to after successful checkout
* @prop {string} checkoutSessionCancelUrl The URL to redirect to if checkout is cancelled
* @prop {string} checkoutSetupSessionSuccessUrl The URL to redirect to after successful setup session
* @prop {string} checkoutSetupSessionCancelUrl The URL to redirect to if setup session is cancelled
* @prop {boolean} testEnv Whether this is a test environment
* @prop {string} webhookSecret The Stripe webhook secret
* @prop {string} webhookHandlerUrl The URL to handle Stripe webhooks
*/
/**
* The `StripeService` contains the core logic for Ghost's Stripe integration.
*/
module.exports = class StripeService {
/**
* @param {object} deps
* @param {*} deps.labs
* @param {*} deps.membersService
* @param {*} deps.donationService
* @param {*} deps.staffService
* @param {import('./WebhookManager').StripeWebhook} deps.StripeWebhook
* @param {object} deps.models
* @param {object} deps.models.Product
* @param {object} deps.models.StripePrice
* @param {object} deps.models.StripeCustomerSubscription
* @param {object} deps.models.StripeProduct
* @param {object} deps.models.MemberStripeCustomer
* @param {object} deps.models.Offer
* @param {object} deps.models.Settings
*/
constructor({
labs,
membersService,
@ -112,6 +147,10 @@ module.exports = class StripeService {
DomainEvents.dispatch(StripeLiveDisabledEvent.create({message: 'Stripe Live Mode Disabled'}));
}
/**
* Configures the Stripe API and registers the webhook with Stripe
* @param {IStripeServiceConfig} config
*/
async configure(config) {
this.api.configure({
secretKey: config.secretKey,

View file

@ -22,13 +22,16 @@ module.exports = class WebhookController {
};
}
/**
* Handles a Stripe webhook event.
* - Parses the webhook event
* - Delegates the event to the appropriate handler
* - Returns a 200 response to Stripe to confirm receipt of the event, or an error response if the event is not handled or if an error occurs
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns {Promise<void>}
*/
async handle(req, res) {
// if (!apiService.configured) {
// logging.error(`Stripe not configured, not handling webhook`);
// res.writeHead(400);
// return res.end();
// }
if (!req.body || !req.headers['stripe-signature']) {
res.writeHead(400);
return res.end();
@ -55,7 +58,10 @@ module.exports = class WebhookController {
}
/**
* Accepts a webhook's event payload and delegates it to the appropriate handler based on the event type
* @private
* @param {import('stripe').Stripe.Event} event
* @returns {Promise<void>}
*/
async handleEvent(event) {
if (!this.handlers[event.type]) {
@ -66,6 +72,8 @@ module.exports = class WebhookController {
}
/**
* Delegates any `customer.subscription.*` events to the `subscriptionEventService`
* @param {import('stripe').Stripe.Subscription} subscription
* @private
*/
async subscriptionEvent(subscription) {
@ -73,6 +81,7 @@ module.exports = class WebhookController {
}
/**
* Delegates any `invoice.*` events to the `invoiceEventService`
* @param {import('stripe').Stripe.Invoice} invoice
* @private
*/
@ -81,6 +90,8 @@ module.exports = class WebhookController {
}
/**
* Delegates any `checkout.session.*` events to the `checkoutSessionEventService`
* @param {import('stripe').Stripe.Checkout.Session} session
* @private
*/
async checkoutSessionEvent(session) {

View file

@ -1,12 +1,5 @@
/**
* @typedef {import('stripe').Stripe.WebhookEndpointCreateParams.EnabledEvent} WebhookEvent
*/
/**
* @typedef {import('stripe').Stripe.WebhookEndpoint} Webhook
*/
/**
* @typedef {import('./StripeAPI')} StripeAPI
*/
@ -18,7 +11,7 @@
/**
* @typedef {object} StripeWebhook
* @prop {(data: StripeWebhookModel) => Promise<StripeWebhookModel>} save
* @prop {(data: StripeWebhookModel) => Promise<void>} save
* @prop {() => Promise<StripeWebhookModel>} get
*/
@ -57,6 +50,8 @@ module.exports = class WebhookManager {
];
/**
* Deletes the Stripe Webhook Endpoint and saves null values for the webhook ID and secret.
*
* @returns {Promise<boolean>}
*/
async stop() {
@ -79,6 +74,11 @@ module.exports = class WebhookManager {
}
}
/**
* Starts the Stripe Webhook Endpoint and saves the webhook ID and secret.
*
* @returns {Promise<void>}
*/
async start() {
if (this.mode !== 'network') {
return;
@ -96,6 +96,7 @@ module.exports = class WebhookManager {
}
/**
* Configures the Stripe Webhook Manager.
* @param {object} config
* @param {string} [config.webhookSecret] An optional webhook secret for use with stripe-cli, passing this will ensure a webhook is not created in Stripe
* @param {string} config.webhookHandlerUrl The URL which the Webhook should hit
@ -111,13 +112,17 @@ module.exports = class WebhookManager {
}
/**
* Setup a new Stripe Webhook Endpoint.
* - If the webhook exists, delete it and create a new one
* - If the webhook does not exist, create a new one
*
* @param {string} [id]
* @param {string} [secret]
* @param {object} [opts]
* @param {boolean} [opts.forceCreate]
* @param {boolean} [opts.skipDelete]
*
* @returns {Promise<Webhook>}
* @returns {Promise<{id: string, secret: string}>}
*/
async setupWebhook(id, secret, opts = {}) {
if (!id || !secret || opts.forceCreate) {
@ -158,6 +163,8 @@ module.exports = class WebhookManager {
}
/**
* Parse a Stripe Webhook event.
*
* @param {string} body
* @param {string} signature
* @returns {import('stripe').Stripe.Event}

View file

@ -3,12 +3,39 @@ const _ = require('lodash');
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
/**
* Handles `checkout.session.completed` webhook events
*
* The `checkout.session.completed` event is triggered when a customer completes a checkout session.
*
* It is triggered for the following scenarios:
* - Subscription
* - Donation
* - Setup intent
*
* This service delegates the event to the appropriate handler based on the session mode and metadata.
*
* The `session` payload can be found here: https://docs.stripe.com/api/checkout/sessions/object
*/
module.exports = class CheckoutSessionEventService {
/**
* @param {object} deps
* @param {import('../StripeAPI')} deps.api
* @param {object} deps.memberRepository
* @param {object} deps.donationRepository
* @param {object} deps.staffServiceEmails
* @param {function} deps.sendSignupEmail
*/
constructor(deps) {
this.api = deps.api;
this.deps = deps;
}
/**
* Handles a `checkout.session.completed` event
* Delegates to the appropriate handler based on the session mode and metadata
* @param {import('stripe').Stripe.Checkout.Session} session
*/
async handleEvent(session) {
if (session.mode === 'setup') {
await this.handleSetupEvent(session);
@ -23,6 +50,10 @@ module.exports = class CheckoutSessionEventService {
}
}
/**
* Handles a `checkout.session.completed` event for a donation
* @param {import('stripe').Stripe.Checkout.Session} session
*/
async handleDonationEvent(session) {
const donationField = session.custom_fields?.find(obj => obj?.key === 'donation_message');
const donationMessage = donationField?.text?.value ? donationField.text.value : null;
@ -54,6 +85,13 @@ module.exports = class CheckoutSessionEventService {
await staffServiceEmails.notifyDonationReceived({donationPaymentEvent: data});
}
/**
* Handles a `checkout.session.completed` event for a setup intent
*
* This is used when a customer adds or changes their payment method outside
* of the normal subscription flow.
* @param {import('stripe').Stripe.Checkout.Session} session
*/
async handleSetupEvent(session) {
const setupIntent = await this.api.getSetupIntent(session.setup_intent);
@ -119,6 +157,10 @@ module.exports = class CheckoutSessionEventService {
}
}
/**
* Handles a `checkout.session.completed` event for a subscription
* @param {import('stripe').Stripe.Checkout.Session} session
*/
async handleSubscriptionEvent(session) {
const customer = await this.api.getCustomer(session.customer, {
expand: ['subscriptions.data.default_payment_method']

View file

@ -1,11 +1,29 @@
const errors = require('@tryghost/errors');
// const _ = require('lodash');
/**
* Handles `invoice.payment_succeeded` webhook events
*
* The `invoice.payment_succeeded` event is triggered when a customer's payment succeeds.
*/
module.exports = class InvoiceEventService {
/**
* @param {object} deps
* @param {object} deps.api
* @param {object} deps.memberRepository
* @param {object} deps.eventRepository
* @param {object} deps.productRepository
*/
constructor(deps) {
this.deps = deps;
}
/**
* Handles a `invoice.payment_succeeded` event
*
* Inserts a payment event into the database
* @param {import('stripe').Stripe.Invoice} invoice
*/
async handleInvoiceEvent(invoice) {
const {api, memberRepository, eventRepository, productRepository} = this.deps;

View file

@ -1,10 +1,29 @@
const errors = require('@tryghost/errors');
const _ = require('lodash');
/**
* Handles `customer.subscription.*` webhook events
*
* The `customer.subscription.*` events are triggered when a customer's subscription status changes.
*
* This service is responsible for handling these events and updating the subscription status in Ghost,
* although it mostly delegates the responsibility to the `MemberRepository`.
*/
module.exports = class SubscriptionEventService {
/**
* @param {object} deps
* @param {import('../../repositories/MemberRepository')} deps.memberRepository
*/
constructor(deps) {
this.deps = deps;
}
/**
* Handles a `customer.subscription.*` event
*
* Looks up the member by the Stripe customer ID and links the subscription to the member.
* @param {import('stripe').Stripe.Subscription} subscription
*/
async handleSubscriptionEvent(subscription) {
const subscriptionPriceData = _.get(subscription, 'items.data');
if (!subscriptionPriceData || subscriptionPriceData.length !== 1) {