mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
Refactoring payments package to handle subscriptions
This commit is contained in:
parent
296d3c9b82
commit
756029c93d
8 changed files with 258 additions and 52 deletions
|
@ -4,7 +4,7 @@ import { Components, registerComponent, getSetting, registerSetting, withCurrent
|
|||
import Users from 'meteor/vulcan:users';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import classNames from 'classnames';
|
||||
import withCreateCharge from '../containers/withCreateCharge.js';
|
||||
import withPaymentAction from '../containers/withPaymentAction.js';
|
||||
import { Products } from '../modules/products.js';
|
||||
|
||||
const stripeSettings = getSetting('stripe');
|
||||
|
@ -22,7 +22,7 @@ class Checkout extends React.Component {
|
|||
|
||||
onToken(token) {
|
||||
|
||||
const {createChargeMutation, productKey, associatedCollection, associatedDocument, callback, properties, currentUser, flash, coupon} = this.props;
|
||||
const {paymentActionMutation, productKey, associatedCollection, associatedDocument, callback, properties, currentUser, flash, coupon} = this.props;
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
|
@ -36,7 +36,7 @@ class Checkout extends React.Component {
|
|||
coupon,
|
||||
}
|
||||
|
||||
createChargeMutation(args).then(response => {
|
||||
paymentActionMutation(args).then(response => {
|
||||
|
||||
// not needed because we just unmount the whole component:
|
||||
this.setState({ loading: false });
|
||||
|
@ -109,7 +109,7 @@ Checkout.contextTypes = {
|
|||
|
||||
const WrappedCheckout = (props) => {
|
||||
const { fragment, fragmentName } = props;
|
||||
const WrappedCheckout = withCreateCharge({fragment, fragmentName})(Checkout);
|
||||
const WrappedCheckout = withPaymentAction({fragment, fragmentName})(Checkout);
|
||||
return <WrappedCheckout {...props}/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ import { graphql } from 'react-apollo';
|
|||
import gql from 'graphql-tag';
|
||||
import { getFragment, getFragmentName } from 'meteor/vulcan:core';
|
||||
|
||||
export default function withCreateCharge(options) {
|
||||
export default function withPaymentAction(options) {
|
||||
|
||||
const fragment = options.fragment || getFragment(options.fragmentName);
|
||||
const fragmentName = getFragmentName(fragment) || fragmentName;
|
||||
|
||||
const mutation = gql`
|
||||
mutation createChargeMutation($token: JSON, $userId: String, $productKey: String, $associatedCollection: String, $associatedId: String, $properties: JSON, $coupon: String) {
|
||||
createChargeMutation(token: $token, userId: $userId, productKey: $productKey, associatedCollection: $associatedCollection, associatedId: $associatedId, properties: $properties, coupon: $coupon) {
|
||||
mutation paymentActionMutation($token: JSON, $userId: String, $productKey: String, $associatedCollection: String, $associatedId: String, $properties: JSON, $coupon: String) {
|
||||
paymentActionMutation(token: $token, userId: $userId, productKey: $productKey, associatedCollection: $associatedCollection, associatedId: $associatedId, properties: $properties, coupon: $coupon) {
|
||||
__typename
|
||||
...${fragmentName}
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ export default function withCreateCharge(options) {
|
|||
`;
|
||||
|
||||
return graphql(mutation, {
|
||||
alias: 'withCreateCharge',
|
||||
alias: 'withPaymentAction',
|
||||
props: ({ownProps, mutate}) => ({
|
||||
createChargeMutation: (vars) => {
|
||||
paymentActionMutation: (vars) => {
|
||||
return mutate({
|
||||
variables: vars,
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
import moment from 'moment';
|
||||
|
||||
const schema = {
|
||||
|
||||
|
@ -24,6 +25,16 @@ const schema = {
|
|||
|
||||
// custom properties
|
||||
|
||||
associatedCollection: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
associatedId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
tokenId: {
|
||||
type: String,
|
||||
optional: false,
|
||||
|
@ -59,6 +70,30 @@ const schema = {
|
|||
optional: true,
|
||||
},
|
||||
|
||||
// GraphQL only
|
||||
|
||||
createdAtFormatted: {
|
||||
type: String,
|
||||
optional: true,
|
||||
resolveAs: {
|
||||
type: 'String',
|
||||
resolver: (charge, args, context) => {
|
||||
return moment(charge.createdAt).format('dddd, MMMM Do YYYY');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stripeChargeUrl: {
|
||||
type: String,
|
||||
optional: true,
|
||||
resolveAs: {
|
||||
type: 'String',
|
||||
resolver: (charge, args, context) => {
|
||||
return `https://dashboard.stripe.com/payments/${charge.data.id}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default schema;
|
||||
|
|
11
packages/vulcan-payments/lib/modules/custom_fields.js
Normal file
11
packages/vulcan-payments/lib/modules/custom_fields.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Users from 'meteor/vulcan:users';
|
||||
|
||||
Users.addField([
|
||||
{
|
||||
fieldName: 'stripeCustomerId',
|
||||
fieldSchema: {
|
||||
type: String,
|
||||
optional: true,
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -4,5 +4,6 @@ import '../components/Checkout.jsx';
|
|||
|
||||
import './routes.js';
|
||||
import './i18n.js';
|
||||
import './custom_fields.js';
|
||||
|
||||
export * from './products.js'
|
|
@ -1,8 +1,8 @@
|
|||
import { getSetting, registerSetting, newMutation, editMutation, Collections, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
|
||||
// import express from 'express';
|
||||
import express from 'express';
|
||||
import Stripe from 'stripe';
|
||||
// import { Picker } from 'meteor/meteorhacks:picker';
|
||||
// import bodyParser from 'body-parser';
|
||||
import { Picker } from 'meteor/meteorhacks:picker';
|
||||
import bodyParser from 'body-parser';
|
||||
import Charges from '../../modules/charges/collection.js';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
import { Products } from '../../modules/products.js';
|
||||
|
@ -11,6 +11,10 @@ registerSetting('stripe', null, 'Stripe settings');
|
|||
|
||||
const stripeSettings = getSetting('stripe');
|
||||
|
||||
// initialize Stripe
|
||||
const keySecret = Meteor.isDevelopment ? stripeSettings.secretKeyTest : stripeSettings.secretKey;
|
||||
const stripe = new Stripe(keySecret);
|
||||
|
||||
const sampleProduct = {
|
||||
amount: 10000,
|
||||
name: 'My Cool Product',
|
||||
|
@ -18,20 +22,21 @@ const sampleProduct = {
|
|||
currency: 'USD',
|
||||
}
|
||||
|
||||
// returns a promise:
|
||||
export const createCharge = async (args) => {
|
||||
/*
|
||||
|
||||
Create new Stripe charge
|
||||
(returns a promise)
|
||||
|
||||
*/
|
||||
export const performAction = async (args) => {
|
||||
|
||||
let collection, document, returnDocument = {};
|
||||
|
||||
const {token, userId, productKey, associatedCollection, associatedId, properties, coupon } = args;
|
||||
const {token, userId, productKey, associatedCollection, associatedId, properties } = args;
|
||||
|
||||
if (!stripeSettings) {
|
||||
throw new Error('Please fill in your Stripe settings');
|
||||
}
|
||||
|
||||
// initialize Stripe
|
||||
const keySecret = Meteor.isDevelopment ? stripeSettings.secretKeyTest : stripeSettings.secretKey;
|
||||
const stripe = new Stripe(keySecret);
|
||||
|
||||
// if an associated collection name and document id have been provided,
|
||||
// get the associated collection and document
|
||||
|
@ -45,25 +50,66 @@ export const createCharge = async (args) => {
|
|||
const definedProduct = Products[productKey];
|
||||
const product = typeof definedProduct === 'function' ? definedProduct(document) : definedProduct || sampleProduct;
|
||||
|
||||
let amount = product.amount;
|
||||
|
||||
// get the user performing the transaction
|
||||
const user = Users.findOne(userId);
|
||||
|
||||
// create Stripe customer
|
||||
const customer = await stripe.customers.create({
|
||||
email: token.email,
|
||||
source: token.id
|
||||
});
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
// create metadata object
|
||||
const metadata = {
|
||||
userId: userId,
|
||||
userName: Users.getDisplayName(user),
|
||||
userProfile: Users.getProfileUrl(user, true),
|
||||
productKey,
|
||||
...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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
// apply discount coupon and add it to metadata, if there is one
|
||||
if (coupon && product.coupons && product.coupons[coupon]) {
|
||||
amount -= product.coupons[coupon];
|
||||
|
@ -83,19 +129,39 @@ export const createCharge = async (args) => {
|
|||
// create Stripe charge
|
||||
const charge = await stripe.charges.create(chargeData);
|
||||
|
||||
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;
|
||||
|
||||
// create charge document for storing in our own Charges collection
|
||||
const chargeDoc = {
|
||||
createdAt: new Date(),
|
||||
userId,
|
||||
tokenId: token.id,
|
||||
type: 'stripe',
|
||||
test: !token.livemode,
|
||||
test: !livemode,
|
||||
data: charge,
|
||||
ip: token.client_ip,
|
||||
associatedCollection,
|
||||
associatedId,
|
||||
properties,
|
||||
productKey,
|
||||
}
|
||||
|
||||
if (token) {
|
||||
chargeDoc.tokenId = token.id;
|
||||
chargeDoc.test = !token.livemode; // get livemode from token if provided
|
||||
chargeDoc.ip = token.client_ip;
|
||||
}
|
||||
// insert
|
||||
const chargeSaved = newMutation({
|
||||
collection: Charges,
|
||||
|
@ -107,12 +173,14 @@ export const createCharge = async (args) => {
|
|||
// update the associated document
|
||||
if (collection && document) {
|
||||
|
||||
// note: assume a single document can have multiple successive charges associated to it
|
||||
const chargeIds = document.chargeIds ? [...document.chargeIds, chargeSaved._id] : [chargeSaved._id];
|
||||
|
||||
let modifier = {
|
||||
$set: {chargeIds},
|
||||
$unset: {}
|
||||
}
|
||||
|
||||
// run collection.charge.sync callbacks
|
||||
modifier = runCallbacks(`${collection._name}.charge.sync`, modifier, document, chargeDoc);
|
||||
|
||||
|
@ -135,30 +203,121 @@ export const createCharge = async (args) => {
|
|||
|
||||
/*
|
||||
|
||||
POST route with Picker
|
||||
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
|
||||
|
||||
*/
|
||||
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
|
||||
|
||||
*/
|
||||
|
||||
// Picker.middleware(bodyParser.text());
|
||||
// const app = express()
|
||||
|
||||
// Picker.route('/charge', function(params, req, res, next) {
|
||||
|
||||
// const body = JSON.parse(req.body);
|
||||
// app.post('/stripe', function(req, res) {
|
||||
// // Retrieve the request's body and parse it as JSON
|
||||
// console.log('////////////// stripe webhook')
|
||||
|
||||
// // console.log(body)
|
||||
// var event_json = JSON.parse(req.body);
|
||||
|
||||
// const { token, userId, productKey, associatedCollection, associatedId } = body;
|
||||
|
||||
// createCharge({
|
||||
// token,
|
||||
// userId,
|
||||
// productKey,
|
||||
// associatedCollection,
|
||||
// associatedId,
|
||||
// callback: (charge) => {
|
||||
// // return Stripe charge
|
||||
// res.end(JSON.stringify(charge));
|
||||
// }
|
||||
// });
|
||||
// console.log(event_json)
|
||||
|
||||
// res.send(200);
|
||||
// });
|
||||
|
||||
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();
|
||||
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from '../modules/index.js';
|
||||
|
||||
import './mutations.js';
|
||||
import './integrations/stripe.js';
|
||||
export * from './integrations/stripe.js';
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation, Collections, addCallback } from 'meteor/vulcan:core';
|
||||
// import Users from 'meteor/vulcan:users';
|
||||
import { createCharge } from '../server/integrations/stripe.js';
|
||||
import { performAction } from '../server/integrations/stripe.js';
|
||||
|
||||
const resolver = {
|
||||
Mutation: {
|
||||
async createChargeMutation(root, args, context) {
|
||||
return await createCharge(args);
|
||||
async paymentActionMutation(root, args, context) {
|
||||
return await performAction(args);
|
||||
},
|
||||
},
|
||||
};
|
||||
addGraphQLResolvers(resolver);
|
||||
addGraphQLMutation('createChargeMutation(token: JSON, userId: String, productKey: String, associatedCollection: String, associatedId: String, properties: JSON, coupon: String) : Chargeable');
|
||||
addGraphQLMutation('paymentActionMutation(token: JSON, userId: String, productKey: String, associatedCollection: String, associatedId: String, properties: JSON, coupon: String) : Chargeable');
|
||||
|
||||
function CreateChargeableUnionType() {
|
||||
const chargeableSchema = `
|
||||
|
|
Loading…
Add table
Reference in a new issue