mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
Changes: - Added the `invoice.updated` and `invoice.voided` events to the `STRIPE_EVENTS_SENT_BEFORE_A_SUBSCRIPTION_RECORD_EXISTS` array in the `receive-from-stripe` webhook.
169 lines
8.1 KiB
JavaScript
Vendored
169 lines
8.1 KiB
JavaScript
Vendored
module.exports = {
|
|
|
|
|
|
friendlyName: 'Receive Stripe subscription events',
|
|
|
|
|
|
description: 'Receive events from Stripe about subscription renewals',
|
|
|
|
|
|
inputs: {
|
|
id: {
|
|
type: 'string',
|
|
description: 'The unique identifier for this Stripe event.',
|
|
moreInfoUrl: 'https://stripe.com/docs/api/events/object#event_object-id',
|
|
required: true,
|
|
},
|
|
type: {
|
|
type: 'string',
|
|
description: 'The type of this Stripe event.',
|
|
moreInfoUrl: 'https://stripe.com/docs/api/events/object#event_object-type',
|
|
required: true,
|
|
},
|
|
data: {
|
|
type: {object: {}},
|
|
description: 'An object containing data associated with this Stripe event.',
|
|
moreInfoUrl: 'https://stripe.com/docs/api/events/object#event_object-data',
|
|
required: true,
|
|
},
|
|
webhookSecret: {
|
|
type: 'string',
|
|
description: 'Used to verify that requests are coming from Stripe.',
|
|
required: true,
|
|
},
|
|
},
|
|
|
|
|
|
exits: {
|
|
success: { description: 'A Stripe event has successfully been received' },
|
|
missingStripeHeader: { description: 'The webhook received a request with no stripe-signature header', responseType: 'unauthorized'},
|
|
},
|
|
|
|
|
|
fn: async function ({id, type, data, webhookSecret}) {
|
|
|
|
let assert = require('assert');
|
|
|
|
if(!this.req.get('stripe-signature')) {
|
|
throw 'missingStripeHeader';
|
|
}
|
|
|
|
if (!sails.config.custom.stripeSubscriptionWebhookSecret) {
|
|
throw new Error('No Stripe webhook secret configured! (Please set `sails.config.custom.stripeSubscriptionWebhookSecret`.)');
|
|
}
|
|
|
|
if (sails.config.custom.stripeSubscriptionWebhookSecret !== webhookSecret) {
|
|
throw new Error('Received unexpected Stripe webhook request with webhookSecret set to: '+webhookSecret);
|
|
}
|
|
|
|
let stripeEventData = data.object;
|
|
|
|
// If this event does not include a subscription ID, we'll ignore it and return a 200 response.
|
|
if(!stripeEventData.subscription) {
|
|
return;
|
|
}
|
|
assert(stripeEventData.customer !== undefined);
|
|
|
|
// Find the subscription record for this event.
|
|
let subscriptionIdToFind = stripeEventData.subscription;
|
|
let subscriptionForThisEvent = await Subscription.findOne({stripeSubscriptionId: subscriptionIdToFind}).populate('user');
|
|
|
|
let STRIPE_EVENTS_SENT_BEFORE_A_SUBSCRIPTION_RECORD_EXISTS = [
|
|
'invoice.created',// Sent when a user submits the billing form on /customers/new-license, before the user's biliing card is charged.
|
|
'invoice.finalized',// Sent when a user submits the billing form on /customers/new-license, before the user's biliing card is charged.
|
|
'invoice.paid',//Sent when a user submits the billing form on /customers/new-license, when the user's biliing card is charged.
|
|
'invoice.payment_succeeded',// Sent when payment for a users subscription is successful. The save-billing-info-and-subscribe action will check for this event before creating a license key.
|
|
'invoice.payment_failed',// Sent when a users subscritpion payment fails. This can happen before we create a license key and save the subscription in the database.
|
|
'invoice.payment_action_required',// Sent when a user's billing card requires additional verification from stripe.
|
|
'invoice.updated',// Sent before an incomplete invoice is voided. (~24 hours after a payment fails)
|
|
'invoice.voided',// Sent when an incomplete invoice is marked as voided. (~24 hours after a payment fails)
|
|
];
|
|
|
|
// If this event is for a subscription that was just created, we won't have a matching Subscription record in the database. This is because we wait until the subscription's invoice is paid to create the record in our database.
|
|
// To handle cases like this, we'll check to see if a User with the provided stripe customer ID exists, and throw an error if it does not exist.
|
|
if(!subscriptionForThisEvent) {
|
|
if(!_.contains(STRIPE_EVENTS_SENT_BEFORE_A_SUBSCRIPTION_RECORD_EXISTS, type)) {
|
|
throw new Error(`The Stripe subscription events webhook received a event for a subscription with stripeSubscriptionId: ${subscriptionIdToFind}, but no matching record was found in our database.`);
|
|
} else {
|
|
let userReferencedInStripeEvent = await User.findOne({stripeCustomerId: stripeEventData.customer});
|
|
if(!userReferencedInStripeEvent){
|
|
throw new Error(`The receive-from-stripe webhook received an event for an invoice (type: ${type}) for a subscription (stripeSubscriptionId: ${subscriptionIdToFind}) but no matching Subscription or User record (stripeCustomerId: ${stripeEventData.customer}) was found in our databse.`);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let userForThisSubscription = subscriptionForThisEvent.user;
|
|
|
|
// If stripe thinks this subscription renews in 7 days, we'll send the user an subscription reminder email.
|
|
if(type === 'invoice.upcoming' && stripeEventData.billing_reason === 'upcoming') {
|
|
// Get the subscription cost per host for the Subscription renewal notification email.
|
|
let subscriptionCostPerHost = Math.floor(subscriptionForThisEvent.subscriptionPrice / subscriptionForThisEvent.numberOfHosts / 12);
|
|
let upcomingBillingAt = stripeEventData.next_payment_attempt * 1000;
|
|
// Send a upcoming subscription renewal email.
|
|
await sails.helpers.sendTemplateEmail.with({
|
|
to: userForThisSubscription.emailAddress,
|
|
from: sails.config.custom.fromEmailAddress,
|
|
fromName: sails.config.custom.fromName,
|
|
subject: 'Your Fleet Premium subscription',
|
|
template: 'email-upcoming-subscription-renewal',
|
|
templateData: {
|
|
firstName: userForThisSubscription.firstName,
|
|
lastName: userForThisSubscription.lastName,
|
|
subscriptionPriceInWholeDollars: subscriptionForThisEvent.subscriptionPrice,
|
|
numberOfHosts: subscriptionForThisEvent.numberOfHosts,
|
|
subscriptionCostPerHost: subscriptionCostPerHost,
|
|
nextBillingAt: upcomingBillingAt,
|
|
}
|
|
});
|
|
|
|
} else if(type === 'invoice.paid' && stripeEventData.billing_reason === 'subscription_cycle') {
|
|
// If the event was triggered by a user's card successfully being charged by Stripe, we'll generate a new license key, update the subscription's database record, and send the user a renewal confirmation email.
|
|
|
|
if(!stripeEventData.lines || !stripeEventData.lines.data[0]) {
|
|
throw new Error(`When the Stripe subscription events webhook received an event for a paid invoice for subscription id: ${subscriptionIdToFind}, the event data object is missing information about the paid invoice. Check the Stripe dashboard to see the data for this event (Stripe event id: ${id})`);
|
|
}
|
|
|
|
// Get the information about the paid invoice from the stripe event.
|
|
let paidInvoiceInformation = stripeEventData.lines.data[0];
|
|
|
|
// Convert the new subscription cycle's period end timestamp from Stripe into a JS timestamp.
|
|
let nextBillingAt = paidInvoiceInformation.period.end * 1000;
|
|
|
|
// Generate a new license key for this subscription
|
|
let newLicenseKeyForThisSubscription = await sails.helpers.createLicenseKey.with({
|
|
numberOfHosts: subscriptionForThisEvent.numberOfHosts,
|
|
organization: subscriptionForThisEvent.user.organization,
|
|
expiresAt: nextBillingAt,
|
|
});
|
|
|
|
// Update the subscription record
|
|
await Subscription.updateOne({id: subscriptionForThisEvent.id}).set({
|
|
fleetLicenseKey: newLicenseKeyForThisSubscription,
|
|
nextBillingAt: nextBillingAt
|
|
});
|
|
|
|
// Send subscription renewal email
|
|
await sails.helpers.sendTemplateEmail.with({
|
|
to: userForThisSubscription.emailAddress,
|
|
from: sails.config.custom.fromEmailAddress,
|
|
fromName: sails.config.custom.fromName,
|
|
subject: 'Your Fleet Premium subscription',
|
|
template: 'email-subscription-renewal-confirmation',
|
|
templateData: {
|
|
firstName: userForThisSubscription.firstName,
|
|
lastName: userForThisSubscription.lastName,
|
|
}
|
|
});
|
|
|
|
}
|
|
// FUTURE: send emails about failed payments. (type === 'invoice.payment_failed' && stripeEventData.billing_reason === 'subscription_cycle')
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
};
|