Vulcan/packages/vulcan-payments/lib/server/integrations/stripe.js

530 lines
15 KiB
JavaScript
Raw Normal View History

import { getSetting, registerSetting, newMutation, editMutation, Collections, registerCallback, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
import express from 'express';
2017-05-31 10:25:13 +09:00
import Stripe from 'stripe';
import Charges from '../../modules/charges/collection.js';
import Users from 'meteor/vulcan:users';
import { Products } from '../../modules/products.js';
import { webAppConnectHandlersUse } from 'meteor/vulcan:core';
import { Promise } from 'meteor/promise';
2017-05-31 10:25:13 +09:00
registerSetting('stripe', null, 'Stripe settings');
2017-05-31 10:25:13 +09:00
const stripeSettings = getSetting('stripe');
// initialize Stripe
const keySecret = Meteor.isDevelopment ? stripeSettings.secretKeyTest : stripeSettings.secretKey;
2018-02-03 13:24:57 -06:00
export const stripe = new Stripe(keySecret);
2017-05-31 10:25:13 +09:00
const sampleProduct = {
amount: 10000,
name: 'My Cool Product',
description: 'This product is awesome.',
currency: 'USD',
};
2017-05-31 10:25:13 +09:00
/*
Create new Stripe charge
(returns a promise)
*/
export const performAction = async (args) => {
2017-05-31 10:25:13 +09:00
let collection, document, returnDocument = {};
2017-05-31 10:25:13 +09:00
const {token, userId, productKey, associatedCollection, associatedId, properties } = args;
2017-05-31 10:25:13 +09:00
if (!stripeSettings) {
throw new Error('Please fill in your Stripe settings');
}
// if an associated collection name and document id have been provided,
// get the associated collection and document
if (associatedCollection && associatedId) {
collection = _.findWhere(Collections, {_name: associatedCollection});
document = collection.findOne(associatedId);
}
// get the product from Products (either object or function applied to doc)
// or default to sample product
const definedProduct = Products[productKey];
const product = typeof definedProduct === 'function' ? definedProduct(document) : definedProduct || sampleProduct;
// get the user performing the transaction
const user = Users.findOne(userId);
const customer = await getCustomer(user, token.id);
2017-05-31 10:25:13 +09:00
2017-07-24 16:06:04 +09:00
// create metadata object
const metadata = {
userId: userId,
userName: Users.getDisplayName(user),
userProfile: Users.getProfileUrl(user, true),
productKey,
2017-07-24 16:06:04 +09:00
...properties
}
if (associatedCollection && associatedId) {
metadata.associatedCollection = associatedCollection;
metadata.associatedId = associatedId;
}
if (product.plan) {
// if product has a plan, subscribe user to it
returnDocument = await subscribeUser({user, customer, product, collection, document, metadata, args});
} else {
// else, perform charge
returnDocument = await createCharge({user, customer, product, collection, document, metadata, args});
}
return returnDocument;
}
/*
Retrieve or create a Stripe customer
*/
export const getCustomer = async (user, id) => {
let customer;
try {
// try retrieving customer from Stripe
customer = await stripe.customers.retrieve(user.stripeCustomerId);
} catch (error) {
// if user doesn't have a stripeCustomerId; or if id doesn't match up with Stripe
// create new customer object
const customerOptions = { email: user.email };
if (id) { customerOptions.source = id; }
customer = await stripe.customers.create(customerOptions);
// add stripe customer id to user object
await editMutation({
collection: Users,
documentId: user._id,
set: {stripeCustomerId: customer.id},
validate: false
});
}
return customer;
}
/*
Create one-time charge.
*/
export const createCharge = async ({user, customer, product, collection, document, metadata, args}) => {
2018-01-25 15:03:03 -06:00
const { /* token, userId, productKey, associatedId, properties, */ coupon } = args;
let amount = product.amount;
2017-07-24 16:06:04 +09:00
// apply discount coupon and add it to metadata, if there is one
if (coupon && product.coupons && product.coupons[coupon]) {
amount -= product.coupons[coupon];
metadata.coupon = coupon;
metadata.discountAmount = product.coupons[coupon];
}
// gather charge data
const chargeData = {
amount,
2017-05-31 10:25:13 +09:00
description: product.description,
currency: product.currency,
customer: customer.id,
2017-07-24 16:06:04 +09:00
metadata
}
// create Stripe charge
const charge = await stripe.charges.create(chargeData);
2017-05-31 10:25:13 +09:00
return processCharge({collection, document, charge, args, user})
}
/*
Process charge on Vulcan's side
*/
export const processCharge = async ({collection, document, charge, args, user}) => {
let returnDocument = {};
const {token, userId, productKey, associatedCollection, associatedId, properties, livemode } = args;
2017-05-31 10:25:13 +09:00
// create charge document for storing in our own Charges collection
const chargeDoc = {
createdAt: new Date(),
userId,
type: 'stripe',
test: !livemode,
2017-05-31 10:25:13 +09:00
data: charge,
associatedCollection,
associatedId,
2017-05-31 10:25:13 +09:00
properties,
productKey,
}
if (token) {
chargeDoc.tokenId = token.id;
chargeDoc.test = !token.livemode; // get livemode from token if provided
chargeDoc.ip = token.client_ip;
}
2017-05-31 10:25:13 +09:00
// insert
const chargeSaved = newMutation({
collection: Charges,
document: chargeDoc,
validate: false,
});
// if an associated collection and id have been provided,
// update the associated document
if (collection && document) {
// note: assume a single document can have multiple successive charges associated to it
2017-05-31 10:25:13 +09:00
const chargeIds = document.chargeIds ? [...document.chargeIds, chargeSaved._id] : [chargeSaved._id];
let modifier = {
$set: {chargeIds},
$unset: {}
}
2017-05-31 10:25:13 +09:00
// run collection.charge.sync callbacks
modifier = runCallbacks(`${collection._name}.charge.sync`, modifier, document, chargeDoc, user);
2017-05-31 10:25:13 +09:00
returnDocument = await editMutation({
2017-05-31 10:25:13 +09:00
collection,
documentId: associatedId,
set: modifier.$set,
unset: modifier.$unset,
validate: false
});
returnDocument.__typename = collection.typeName;
2017-05-31 10:25:13 +09:00
}
runCallbacksAsync(`${collection._name}.charge.async`, returnDocument, chargeDoc, user);
return returnDocument;
2017-05-31 10:25:13 +09:00
}
/*
Subscribe a user to a Stripe plan
2017-05-31 10:25:13 +09:00
*/
export const subscribeUser = async ({user, customer, product, collection, document, metadata, args }) => {
try {
// if product has an initial cost,
// create an invoice item and attach it to the customer first
// see https://stripe.com/docs/subscriptions/invoices#adding-invoice-items
if (product.initialAmount) {
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-unused-vars
const initialInvoiceItem = await stripe.invoiceItems.create({
customer: customer.id,
amount: product.initialAmount,
currency: product.currency,
description: product.initialAmountDescription,
});
}
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-unused-vars
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [
{ plan: product.plan },
],
metadata,
});
} catch (error) {
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('// Stripe subscribeUser error');
// eslint-disable-next-line no-console
console.log(error);
}
};
// create a stripe plan
// plan is used as the unique ID and is not needed for creating a plan
export const createPlan = async ({
// Extract all the known properties for the stripe api
// Evertying else goes in the metadata field
plan: id,
currency,
interval,
name,
amount,
interval_count,
statement_descriptor,
...metadata
}) => stripe.plans.create({
id,
currency,
interval,
name,
amount,
interval_count,
statement_descriptor,
...metadata
});
export const retrievePlan = async (planObject) => stripe.plans.retrieve(planObject.plan);
export const createOrRetrievePlan = async (planObject) => {
return retrievePlan(planObject)
.catch(error => {
// Plan does not exist, create it
if (error.statusCode === 404) {
// eslint-disable-next-line no-console
console.log(`Creating subscription plan ${planObject.plan} for ${(planObject.amount && (planObject.amount / 100).toLocaleString('en-US', { style: 'currency', currency })) || 'free'}`);
return createPlan(planObject);
}
// Something else went wrong
// eslint-disable-next-line no-console
console.error(error);
throw error;
});
};
/*
Webhooks with Express
2017-05-31 10:25:13 +09:00
*/
2017-05-31 10:25:13 +09:00
// see https://github.com/stripe/stripe-node/blob/master/examples/webhook-signing/express.js
2017-05-31 10:25:13 +09:00
const app = express();
2017-05-31 10:25:13 +09:00
// Add the raw text body of the request to the `request` object
function addRawBody(req, res, next) {
req.setEncoding('utf8');
2017-05-31 10:25:13 +09:00
var data = '';
2017-05-31 10:25:13 +09:00
req.on('data', function(chunk) {
data += chunk;
});
2017-05-31 10:25:13 +09:00
req.on('end', function() {
req.rawBody = data;
next();
});
}
app.use(addRawBody);
app.post('/stripe', async function(req, res) {
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('////////////// stripe webhook');
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(req.rawBody, sig, stripeSettings.endpointSecret);
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('event ///////////////////');
// eslint-disable-next-line no-console
console.log(event);
switch (event.type) {
case 'charge.succeeded':
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('////// charge succeeded');
const charge = event.data.object;
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log(charge);
try {
// look up corresponding invoice
const invoice = await stripe.invoices.retrieve(charge.invoice);
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('////// invoice');
// eslint-disable-next-line no-console
console.log(invoice);
// look up corresponding subscription
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('////// subscription');
// eslint-disable-next-line no-console
console.log(subscription);
const { userId, productKey, associatedCollection, associatedId } = subscription.metadata;
if (associatedCollection && associatedId) {
const collection = _.findWhere(Collections, {_name: associatedCollection});
const document = collection.findOne(associatedId);
const args = {
userId,
productKey,
associatedCollection,
associatedId,
livemode: subscription.livemode,
}
processCharge({ collection, document, charge, args});
}
} catch (error) {
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('// Stripe webhook error');
// eslint-disable-next-line no-console
console.log(error);
}
break;
}
} catch (error) {
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log('///// Stripe webhook error');
// eslint-disable-next-line no-console
console.log(error);
}
res.sendStatus(200);
});
webAppConnectHandlersUse(Meteor.bindEnvironment(app), {name: 'stripe_endpoint', order: 100});
// Picker.middleware(bodyParser.json());
// Picker.route('/stripe', async function(params, req, res, next) {
// console.log('////////////// stripe webhook')
// console.log(req)
// const sig = req.headers['stripe-signature'];
// const body = req.body;
// console.log('sig ///////////////////')
// console.log(sig)
// console.log('body ///////////////////')
// console.log(body)
// console.log('rawBody ///////////////////')
// console.log(req.rawBody)
// try {
// const event = stripe.webhooks.constructEvent(req.rawBody, sig, stripeSettings.endpointSecret);
// console.log('event ///////////////////')
// console.log(event)
// } catch (error) {
// console.log('///// Stripe webhook error')
// console.log(error)
// }
// // Retrieve the request's body and parse it as JSON
// switch (body.type) {
// case 'charge.succeeded':
// console.log('////// charge succeeded')
// // console.log(body)
// const charge = body.data.object;
// try {
// // look up corresponding invoice
// const invoice = await stripe.invoices.retrieve(body.data.object.invoice);
// console.log('////// invoice')
// // look up corresponding subscription
// const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
// console.log('////// subscription')
// console.log(subscription)
// const { userId, productKey, associatedCollection, associatedId } = subscription.metadata;
// if (associatedCollection && associatedId) {
// const collection = _.findWhere(Collections, {_name: associatedCollection});
// const document = collection.findOne(associatedId);
// const args = {
// userId,
// productKey,
// associatedCollection,
// associatedId,
// livemode: subscription.livemode,
// }
// processCharge({ collection, document, charge, args});
// }
// } catch (error) {
// console.log('// Stripe webhook error')
// console.log(error)
// }
// break;
// }
// res.statusCode = 200;
// res.end();
// });
Meteor.startup(() => {
Collections.forEach(c => {
2018-01-25 15:03:03 -06:00
const collectionName = c._name.toLowerCase();
registerCallback({
name: `${collectionName}.charge.sync`,
description: `Modify the modifier used to add charge ids to the charge's associated document.`,
arguments: [{modifier: 'The modifier'}, {document: 'The associated document'}, {charge: 'The charge'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: 'modifier',
});
registerCallback({
name: `${collectionName}.charge.sync`,
description: `Perform operations after the charge has succeeded.`,
arguments: [{document: 'The associated document'}, {charge: 'The charge'}, {currentUser: 'The current user'}],
runs: 'async',
});
});
// Create plans if they don't exist
if (stripeSettings.createPlans) {
// eslint-disable-next-line no-console
console.log('Creating stripe plans...');
Promise.awaitAll(Object.keys(Products)
// Return the object
.map(productKey => {
const definedProduct = Products[productKey];
const product = typeof definedProduct === 'function' ? definedProduct(document) : definedProduct || sampleProduct;
return product;
})
// Find only products that have a plan defined
.filter(productObject => productObject.plan)
.map(planObject => createOrRetrievePlan(planObject)));
// eslint-disable-next-line no-console
console.log('Finished creating stripe plans.');
}
});