2019-10-02 06:36:54 +03:00
|
|
|
const debug = require('ghost-ignition').debug('stripe');
|
|
|
|
const {retrieve, list, create, del} = require('./api/stripeRequests');
|
2019-09-06 08:13:35 +03:00
|
|
|
const api = require('./api');
|
|
|
|
|
2019-09-25 07:35:58 +03:00
|
|
|
const STRIPE_API_VERSION = '2019-09-09';
|
|
|
|
|
2019-09-06 08:13:35 +03:00
|
|
|
module.exports = class StripePaymentProcessor {
|
2019-09-25 06:15:31 +03:00
|
|
|
constructor(config, storage) {
|
|
|
|
this.storage = storage;
|
2019-09-06 08:13:35 +03:00
|
|
|
this._ready = new Promise((resolve, reject) => {
|
|
|
|
this._resolveReady = resolve;
|
|
|
|
this._rejectReady = reject;
|
|
|
|
});
|
|
|
|
this._configure(config);
|
|
|
|
}
|
|
|
|
|
|
|
|
async ready() {
|
|
|
|
return this._ready;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _configure(config) {
|
|
|
|
this._stripe = require('stripe')(config.secretKey);
|
2019-09-25 07:35:58 +03:00
|
|
|
this._stripe.setAppInfo(config.appInfo);
|
|
|
|
this._stripe.setApiVersion(STRIPE_API_VERSION);
|
2019-09-06 08:13:35 +03:00
|
|
|
this._stripe.__TEST_MODE__ = config.secretKey.startsWith('sk_test_');
|
|
|
|
this._public_token = config.publicKey;
|
|
|
|
this._checkoutSuccessUrl = config.checkoutSuccessUrl;
|
|
|
|
this._checkoutCancelUrl = config.checkoutCancelUrl;
|
2019-09-24 13:20:59 +03:00
|
|
|
this._webhookHandlerUrl = config.webhookHandlerUrl;
|
2019-09-06 08:13:35 +03:00
|
|
|
|
|
|
|
try {
|
|
|
|
this._product = await api.products.ensure(this._stripe, config.product);
|
|
|
|
|
2019-09-24 13:20:59 +03:00
|
|
|
this._plans = [];
|
|
|
|
for (const planSpec of config.plans) {
|
2019-09-06 08:13:35 +03:00
|
|
|
const plan = await api.plans.ensure(this._stripe, planSpec, this._product);
|
|
|
|
this._plans.push(plan);
|
2019-09-24 13:20:59 +03:00
|
|
|
}
|
|
|
|
|
2019-10-02 06:36:54 +03:00
|
|
|
const webhooks = await list(this._stripe, 'webhookEndpoints', {
|
|
|
|
limit: 100
|
|
|
|
});
|
|
|
|
|
|
|
|
const webhookToDelete = webhooks.data.find((webhook) => {
|
|
|
|
return webhook.url === this._webhookHandlerUrl;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (webhookToDelete) {
|
|
|
|
await del(this._stripe, 'webhookEndpoints', webhookToDelete.id);
|
|
|
|
}
|
|
|
|
|
2019-09-24 13:20:59 +03:00
|
|
|
try {
|
|
|
|
const webhook = await create(this._stripe, 'webhookEndpoints', {
|
|
|
|
url: this._webhookHandlerUrl,
|
2019-09-25 07:35:58 +03:00
|
|
|
api_version: STRIPE_API_VERSION,
|
2019-09-24 13:20:59 +03:00
|
|
|
enabled_events: ['checkout.session.completed']
|
|
|
|
});
|
|
|
|
this._webhookSecret = webhook.secret;
|
2019-09-06 08:13:35 +03:00
|
|
|
} catch (err) {
|
2019-09-24 13:20:59 +03:00
|
|
|
console.log(err);
|
|
|
|
this._webhookSecret = process.env.WEBHOOK_SECRET;
|
2019-09-06 08:13:35 +03:00
|
|
|
}
|
2019-09-24 13:20:59 +03:00
|
|
|
} catch (err) {
|
|
|
|
return this._rejectReady(err);
|
2019-09-06 08:13:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return this._resolveReady({
|
|
|
|
product: this._product,
|
|
|
|
plans: this._plans
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-24 13:24:28 +03:00
|
|
|
async parseWebhook(body, signature) {
|
|
|
|
return this._stripe.webhooks.constructEvent(body, signature, this._webhookSecret);
|
|
|
|
}
|
|
|
|
|
2019-09-06 08:13:35 +03:00
|
|
|
async createCheckoutSession(member, planName) {
|
2019-09-25 06:41:58 +03:00
|
|
|
let customer;
|
|
|
|
if (member) {
|
|
|
|
customer = await this._customerForMember(member);
|
|
|
|
} else {
|
|
|
|
customer = null;
|
|
|
|
}
|
2019-09-06 08:13:35 +03:00
|
|
|
const plan = this._plans.find(plan => plan.nickname === planName);
|
|
|
|
const session = await this._stripe.checkout.sessions.create({
|
|
|
|
payment_method_types: ['card'],
|
|
|
|
success_url: this._checkoutSuccessUrl,
|
|
|
|
cancel_url: this._checkoutCancelUrl,
|
2019-09-25 06:41:58 +03:00
|
|
|
customer: customer ? customer.id : undefined,
|
2019-09-06 08:13:35 +03:00
|
|
|
subscription_data: {
|
|
|
|
items: [{
|
|
|
|
plan: plan.id
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-09-25 06:41:58 +03:00
|
|
|
return {
|
|
|
|
sessionId: session.id,
|
|
|
|
publicKey: this._public_token
|
|
|
|
};
|
2019-09-06 08:13:35 +03:00
|
|
|
}
|
2019-09-24 13:27:53 +03:00
|
|
|
|
2019-09-25 06:24:45 +03:00
|
|
|
async getActiveSubscriptions(member) {
|
|
|
|
const metadata = await this.storage.get(member);
|
|
|
|
|
|
|
|
const customers = await Promise.all(metadata.map((data) => {
|
|
|
|
return this.getCustomer(data.customer_id);
|
|
|
|
}));
|
|
|
|
|
|
|
|
return customers.reduce(function (subscriptions, customer) {
|
|
|
|
if (customer.deleted) {
|
|
|
|
return subscriptions;
|
|
|
|
}
|
|
|
|
return subscriptions.concat(customer.subscriptions.data.reduce(function (subscriptions, subscription) {
|
|
|
|
// Subscription has more than one plan
|
|
|
|
// was not created by us - ignore it.
|
|
|
|
if (!subscription.plan) {
|
|
|
|
return subscriptions;
|
|
|
|
}
|
|
|
|
// Ignore cancelled subscriptions
|
|
|
|
if (subscription.status === 'cancelled') {
|
|
|
|
return subscriptions;
|
|
|
|
}
|
|
|
|
// Ignore unpaid subscriptions
|
|
|
|
if (subscription.status === 'unpaid') {
|
|
|
|
return subscriptions;
|
|
|
|
}
|
|
|
|
|
|
|
|
return subscriptions.concat([{
|
|
|
|
customer: customer.id,
|
|
|
|
subscription: subscription.id,
|
|
|
|
plan: subscription.plan.id,
|
|
|
|
name: subscription.plan.nickname,
|
|
|
|
amount: subscription.plan.amount,
|
|
|
|
validUntil: subscription.current_period_end
|
|
|
|
}]);
|
|
|
|
}, []));
|
|
|
|
}, []);
|
|
|
|
}
|
|
|
|
|
2019-09-25 06:23:01 +03:00
|
|
|
async addCustomerToMember(member, customer) {
|
|
|
|
const metadata = await this.storage.get(member);
|
2019-09-25 06:29:27 +03:00
|
|
|
// Do not add the customer if they are already linked
|
|
|
|
if (metadata.some(data => data.customer_id === customer.id)) {
|
|
|
|
return;
|
|
|
|
}
|
2019-09-25 06:23:01 +03:00
|
|
|
return this.storage.set(member, metadata.concat({
|
|
|
|
customer_id: customer.id
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2019-09-25 06:23:25 +03:00
|
|
|
async _customerForMember(member) {
|
|
|
|
const metadata = await this.storage.get(member);
|
|
|
|
|
|
|
|
for (const data in metadata) {
|
2019-10-02 06:36:13 +03:00
|
|
|
try {
|
|
|
|
const customer = await this.getCustomer(data.customer_id);
|
|
|
|
if (!customer.deleted) {
|
|
|
|
return customer;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
2019-09-25 06:23:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const customer = await create(this._stripe, 'customers', {
|
|
|
|
email: member.email
|
|
|
|
});
|
|
|
|
|
|
|
|
await this.addCustomerToMember(member, customer);
|
|
|
|
|
|
|
|
return customer;
|
|
|
|
}
|
|
|
|
|
2019-09-24 13:27:53 +03:00
|
|
|
async getCustomer(id) {
|
|
|
|
return retrieve(this._stripe, 'customers', id);
|
|
|
|
}
|
2019-09-06 08:13:35 +03:00
|
|
|
};
|