2017-09-22 12:24:15 +02:00
|
|
|
import { getSetting, registerSetting, newMutation, editMutation, Collections, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
|
2017-10-18 20:05:51 +09:00
|
|
|
import express from 'express';
|
2017-05-31 10:25:13 +09:00
|
|
|
import Stripe from 'stripe';
|
2017-10-18 20:05:51 +09:00
|
|
|
import { Picker } from 'meteor/meteorhacks:picker';
|
|
|
|
import bodyParser from 'body-parser';
|
2017-05-31 10:25:13 +09:00
|
|
|
import Charges from '../../modules/charges/collection.js';
|
|
|
|
import Users from 'meteor/vulcan:users';
|
|
|
|
import { Products } from '../../modules/products.js';
|
|
|
|
|
2017-09-22 12:24:15 +02:00
|
|
|
registerSetting('stripe', null, 'Stripe settings');
|
|
|
|
|
2017-05-31 10:25:13 +09:00
|
|
|
const stripeSettings = getSetting('stripe');
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// initialize Stripe
|
|
|
|
const keySecret = Meteor.isDevelopment ? stripeSettings.secretKeyTest : stripeSettings.secretKey;
|
|
|
|
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-10-18 20:05:51 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Create new Stripe charge
|
|
|
|
(returns a promise)
|
|
|
|
|
|
|
|
*/
|
|
|
|
export const performAction = async (args) => {
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-06-04 13:56:40 +09:00
|
|
|
let collection, document, returnDocument = {};
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +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);
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
let customer;
|
|
|
|
|
|
|
|
if (user.stripeCustomerId) {
|
|
|
|
// if user has a stripe id already, retrieve customer from Stripe
|
|
|
|
customer = await stripe.customers.retrieve(user.stripeCustomerId);
|
|
|
|
} else {
|
|
|
|
// else create new Stripe customer
|
|
|
|
customer = await stripe.customers.create({
|
|
|
|
email: token.email,
|
|
|
|
source: token.id
|
|
|
|
});
|
|
|
|
|
|
|
|
// add stripe customer id to user object
|
|
|
|
await editMutation({
|
|
|
|
collection: Users,
|
|
|
|
documentId: user._id,
|
|
|
|
set: {stripeCustomerId: customer.id},
|
|
|
|
validate: false
|
|
|
|
});
|
|
|
|
}
|
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),
|
2017-10-18 20:05:51 +09:00
|
|
|
productKey,
|
2017-07-24 16:06:04 +09:00
|
|
|
...properties
|
|
|
|
}
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Create one-time charge.
|
|
|
|
|
|
|
|
*/
|
|
|
|
export const createCharge = async ({user, customer, product, collection, document, metadata, args}) => {
|
|
|
|
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
2017-07-23 16:29:21 +09:00
|
|
|
// 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
|
2017-07-23 16:29:21 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// create Stripe charge
|
|
|
|
const charge = await stripe.charges.create(chargeData);
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
return processCharge({collection, document, charge, args})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Process charge on Vulcan's side
|
|
|
|
|
|
|
|
*/
|
|
|
|
export const processCharge = async ({collection, document, charge, args}) => {
|
|
|
|
|
|
|
|
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',
|
2017-10-18 20:05:51 +09:00
|
|
|
test: !livemode,
|
2017-05-31 10:25:13 +09:00
|
|
|
data: charge,
|
2017-10-18 20:05:51 +09:00
|
|
|
associatedCollection,
|
|
|
|
associatedId,
|
2017-05-31 10:25:13 +09:00
|
|
|
properties,
|
|
|
|
productKey,
|
|
|
|
}
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
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) {
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// 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-10-18 20:05:51 +09:00
|
|
|
|
2017-05-31 10:25:13 +09:00
|
|
|
// run collection.charge.sync callbacks
|
|
|
|
modifier = runCallbacks(`${collection._name}.charge.sync`, modifier, document, chargeDoc);
|
|
|
|
|
2017-08-07 16:02:29 +09:00
|
|
|
returnDocument = await editMutation({
|
2017-05-31 10:25:13 +09:00
|
|
|
collection,
|
|
|
|
documentId: associatedId,
|
|
|
|
set: modifier.$set,
|
|
|
|
unset: modifier.$unset,
|
|
|
|
validate: false
|
|
|
|
});
|
|
|
|
|
2017-06-04 13:56:40 +09:00
|
|
|
returnDocument.__typename = collection.typeName;
|
2017-05-31 10:25:13 +09:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2017-06-04 13:56:40 +09:00
|
|
|
runCallbacksAsync(`${collection._name}.charge.async`, returnDocument, chargeDoc);
|
|
|
|
|
|
|
|
return returnDocument;
|
2017-05-31 10:25:13 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
Create new subscription plan
|
|
|
|
|
|
|
|
*/
|
|
|
|
export const createPlan = async (options) => {
|
|
|
|
try {
|
|
|
|
await stripe.plans.create(options);
|
|
|
|
} catch (error) {
|
|
|
|
console.log('// Stripe createPlan error')
|
|
|
|
console.log(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Subscribe a user to a Stripe plan
|
2017-05-31 10:25:13 +09:00
|
|
|
|
|
|
|
*/
|
2017-10-18 20:05:51 +09:00
|
|
|
export const subscribeUser = async ({user, customer, product, collection, document, metadata, args }) => {
|
|
|
|
|
|
|
|
console.log('////////////// subscribeUser')
|
|
|
|
console.log(product)
|
|
|
|
|
|
|
|
try {
|
|
|
|
const subscription = await stripe.subscriptions.create({
|
|
|
|
customer: customer.id,
|
|
|
|
items: [
|
|
|
|
{ plan: product.plan },
|
|
|
|
],
|
|
|
|
metadata,
|
|
|
|
});
|
|
|
|
console.log(subscription)
|
|
|
|
} catch (error) {
|
|
|
|
console.log('// Stripe subscribeUser error')
|
|
|
|
console.log(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Webhooks with Picker
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
*/
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// const app = express()
|
2017-05-31 10:25:13 +09:00
|
|
|
|
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// app.post('/stripe', function(req, res) {
|
|
|
|
// // Retrieve the request's body and parse it as JSON
|
|
|
|
// console.log('////////////// stripe webhook')
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// var event_json = JSON.parse(req.body);
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// console.log(event_json)
|
2017-05-31 10:25:13 +09:00
|
|
|
|
2017-10-18 20:05:51 +09:00
|
|
|
// res.send(200);
|
2017-05-31 10:25:13 +09:00
|
|
|
// });
|
2017-10-18 20:05:51 +09:00
|
|
|
|
|
|
|
Picker.middleware(bodyParser.json());
|
|
|
|
|
|
|
|
Picker.route('/stripe', async function(params, req, res, next) {
|
|
|
|
|
|
|
|
console.log('////////////// stripe webhook')
|
|
|
|
|
|
|
|
const body = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// Retrieve the request's body and parse it as JSON
|
|
|
|
switch (body.type) {
|
|
|
|
|
|
|
|
case 'charge.succeeded':
|
|
|
|
|
|
|
|
console.log('////// charge body')
|
|
|
|
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();
|
|
|
|
|
|
|
|
});
|