Added WebhookManager and StripeService modules

no-issue
This commit is contained in:
Fabien "egg" O'Carroll 2022-01-14 11:05:19 +02:00
parent 3a1c51672c
commit 635aa8aa3f
2 changed files with 227 additions and 0 deletions

View File

@ -0,0 +1,59 @@
const WebhookManager = require('./WebhookManager');
const StripeAPI = require('./StripeAPI');
const StripeMigrations = require('./Migrations');
module.exports = class StripeService {
constructor({
StripeWebhook,
models
}) {
const api = new StripeAPI();
const webhookManager = new WebhookManager({
StripeWebhook,
api
});
const migrations = new StripeMigrations({
models,
api
});
this.models = models;
this.api = api;
this.webhookManager = webhookManager;
this.migrations = migrations;
}
async connect() {
}
async disconnect() {
await this.models.Product.forge().query().update({
monthly_price_id: null,
yearly_price_id: null
});
await this.models.StripePrice.forge().query().del();
await this.models.StripeProduct.forge().query().del();
await this.models.MemberStripeCustomer.forge().query().del();
await this.models.Offer.forge().query().update({
stripe_coupon_id: null
});
await this.webhookManager.stop();
}
async configure(config) {
this.api.configure({
secretKey: config.secretKey,
publicKey: config.publicKey,
enablePromoCodes: config.enablePromoCodes
});
console.log('finna setup webhooks');
console.log(config.webhookSecret, config.webhookHandlerUrl);
await this.webhookManager.configure({
webhookSecret: config.webhookSecret,
webhookHandlerUrl: config.webhookHandlerUrl
});
await this.webhookManager.start();
console.log('webhooks done');
}
};

View File

@ -0,0 +1,168 @@
/**
* @typedef {import('stripe').Stripe.WebhookEndpointCreateParams.EnabledEvent} WebhookEvent
*/
/**
* @typedef {import('stripe').Stripe.WebhookEndpoint} Webhook
*/
/**
* @typedef {import('./StripeAPI')} StripeAPI
*/
/**
* @typedef {object} StripeWebhookModel
* @prop {string} webhook_id
* @prop {string} secret
*/
/**
* @typedef {object} StripeWebhook
* @prop {(data: StripeWebhookModel) => Promise<StripeWebhookModel>} save
* @prop {() => Promise<StripeWebhookModel>} get
*/
module.exports = class WebhookManager {
/**
* @param {object} deps
* @param {StripeWebhook} deps.StripeWebhook
* @param {StripeAPI} deps.api
*/
constructor({
StripeWebhook,
api
}) {
/** @private */
this.StripeWebhook = StripeWebhook;
/** @private */
this.api = api;
/** @private */
this.config = null;
/** @private */
this.webhookSecret = null;
/**
* @private
* @type {'network'|'local'}
*/
this.mode = 'network';
}
/** @type {WebhookEvent[]} */
static events = [
'checkout.session.completed',
'customer.subscription.deleted',
'customer.subscription.updated',
'customer.subscription.created',
'invoice.payment_succeeded'
];
/**
* @returns {Promise<boolean>}
*/
async stop() {
if (this.mode !== 'network') {
return;
}
try {
const existingWebhook = await this.StripeWebhook.get();
if (existingWebhook.webhook_id) {
await this.api.deleteWebhookEndpoint(existingWebhook.webhook_id);
}
await this.StripeWebhook.save({
webhook_id: null,
secret: null
});
return true;
} catch (err) {
return false;
}
}
async start() {
if (this.mode !== 'network') {
return;
}
const existingWebhook = await this.StripeWebhook.get();
const webhook = await this.setupWebhook(existingWebhook.webhook_id, existingWebhook.secret);
await this.StripeWebhook.save({
webhook_id: webhook.id,
secret: webhook.secret
});
this.webhookSecret = webhook.secret;
}
/**
* @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
*
* @returns {Promise<void>}
*/
async configure(config) {
this.config = config;
if (config.webhookSecret) {
this.webhookSecret = config.webhookSecret;
this.mode = 'local';
}
}
/**
* @param {string=} id
* @param {string=} secret
* @param {object=} opts
* @param {boolean} opts.forceCreate
* @param {boolean} opts.skipDelete
*
* @returns {Promise<Webhook>}
*/
async setupWebhook(id, secret, opts) {
if (!id || !secret || opts.forceCreate) {
if (id && !opts.skipDelete) {
try {
await this.api.deleteWebhookEndpoint(id);
} catch (err) {
// Continue
}
}
const webhook = await this.api.createWebhookEndpoint(
this.config.webhookHandlerUrl,
WebhookManager.events
);
return {
id: webhook.id,
secret: webhook.secret
};
} else {
try {
await this.api.updateWebhookEndpoint(
id,
this.config.webhookHandlerUrl,
WebhookManager.events
);
return {
id,
secret
};
} catch (err) {
if (err.code === 'resource_missing') {
return this.setupWebhook(id, secret, {skipDelete: true, forceCreate: true});
}
return this.setupWebhook(id, secret, {skipDelete: false, forceCreate: true});
}
}
}
/**
* @param {string} body
* @param {string} signature
* @returns {import('stripe').Stripe.Event}
*/
parseWebhook(body, signature) {
return this.api.parseWebhook(body, signature, this.webhookSecret);
}
};