Permalink
Browse files

WE CAN HAZ PRO?

  • Loading branch information...
1 parent d6a27f5 commit 8e74e669ebadadaefbbcc9859b8f89c8a49b228c @remy remy committed Jul 11, 2014
@@ -0,0 +1 @@
+ALTER TABLE customers ADD COLUMN plan varchar(255);
View
@@ -7,6 +7,10 @@
"name": "Private Bins",
"user": "pro"
},
+ {
+ "name": "SSL",
+ "user": "pro"
+ },
{
"name": "Dropbox Sync",
"user": "pro"
View
@@ -494,15 +494,16 @@ module.exports = utils.inherit(Object, {
var values = [
params.stripeId,
params.userId || null,
- params.user
+ params.user,
+ params.plan,
], sql = templates.setCustomer;
// not using callbacks atm
fn = fn || function() {};
this.connection.query(sql, values, function (err, result) {
if (err || !result.affectedRows) {
- return fn(err);
+ return fn(err || new Error('unable to set customer'));
}
fn(null, result);
});
@@ -529,7 +530,7 @@ module.exports = utils.inherit(Object, {
var sql = templates.getCustomerByUser;
this.connection.query(sql, [user.name], function(err, result) {
if (err || !result) {
- return fn(err);
+ return fn(err || new Error('unable to find customer'));
}
fn(null, result);
});
@@ -39,7 +39,7 @@
"isOwnerOf": "SELECT name=? as `owner`, s.active FROM `owners` AS `o`, `sandbox` AS `s` WHERE o.url=s.url AND o.revision=s.revision AND o.url=? AND o.revision=1",
"getUserBinCount": "SELECT COUNT(*) as total FROM `owners` WHERE `name`=?",
"setProAccount": "UPDATE ownership SET `pro`=?, `updated`=? WHERE `name`=?",
- "setCustomer" : "REPLACE INTO `customers` (`stripe_id`, `user_id`, `name`) VALUES (?, ?, ?)",
+ "setCustomer" : "REPLACE INTO `customers` (`stripe_id`, `user_id`, `name`, `plan`) VALUES (?, ?, ?, ?)",
"setCustomerActive": "UPDATE `customers` SET `active`=? WHERE `name`=?",
"getCustomerByStripeId": "SELECT * FROM `customers` WHERE `stripe_id`=? LIMIT 1",
"getCustomerByUser": "SELECT * FROM `customers` WHERE `name`=? LIMIT 1",
View
@@ -1,19 +1,35 @@
'use strict';
-
var undefsafe = require('undefsafe');
var config = require('../config');
+var stripeKey = undefsafe(config, 'payment.stripe.public');
+var models = require('../models');
+var metrics = require('./metrics');
+var customer = models.customer;
+var vatValidator = require('validate-vat');
var featureList = require('../data/features.json');
var backersList = require('../data/backers.json');
+var Promise = require('promise'); // jshint ignore:line
+var util = require('util');
+var stripe;
+
+var debug = config.environment !== 'production';
+if (stripeKey) {
+ stripe = require('stripe')(undefsafe(config, 'payment.stripe.secret'));
+}
+
+// this is a compatibility layer for our handler index.js magic
var upgrade = module.exports = {
name: 'upgrade'
};
-upgrade.features = function (req, res) {
- var stripeKey = undefsafe(config, 'payment.stripe.public');
- var stripeProMonthURL = undefsafe(config, 'payment.stripe.urls.month');
+upgrade.features = function (req, res, next) {
+ if (!stripeKey) {
+ return next('route');
+ }
var app = req.app;
+ var stripeProMonthURL = undefsafe(config, 'payment.stripe.urls.month');
res.render('upgrade', {
layout: 'sub/layout',
@@ -29,34 +45,217 @@ upgrade.features = function (req, res) {
});
};
-upgrade.payment = function (req, res) {
+upgrade.payment = function (req, res, next) {
+ if (!stripeKey) {
+ return next('route');
+ }
+
+ if (!req.body) {
+ metrics.increment('upgrade.view');
+ } else {
+ metrics.increment('upgrade.try-again');
+ }
+
var app = req.app;
- var stripeKey = undefsafe(config, 'payment.stripe.public');
- var stripeProMonthURL = undefsafe(config, 'payment.stripe.urls.month');
- var stripeProYearURL = undefsafe(config, 'payment.stripe.urls.year');
+ var user = undefsafe(req, 'session.user') || {};
+
+ var info = req.flash(req.flash.INFO);
+ var error = req.flash(req.flash.ERROR);
+ var notification = req.flash(req.flash.NOTIFICATION);
+
+ var flash = error || notification || info;
res.render('payment', {
+ flash: flash,
request: req,
- user: undefsafe(req, 'session.user'),
layout: 'sub/layout',
root: app.locals.url('', true, req.secure),
static: app.locals.urlForStatic('', req.secure),
referrer: req.get('referer'),
- featureList: featureList,
- backersList: backersList,
+ csrf: req.session._csrf,
+ values: {
+ email: req.body.email || user.email,
+ vat: req.body.vat,
+ country: req.body.country,
+ subscription: req.body.subscription,
+ number: req.body.number,
+ expiry: req.body.expiry,
+ cvc: req.body.cvc,
+ buyer_type: req.body.buyer_type, // jshint ignore:line
+ },
stripe: {
key: stripeKey,
- monthly: stripeProMonthURL,
- yearly: stripeProYearURL
},
cachebust: app.set('is_production') ? '?' + req.app.set('version') : '',
- showCoupon: req.query.coupon === 'true'
+ showCoupon: req.query.coupon === 'true', // TODO!
});
};
+upgrade.processPayment = function (req, res, next) {
+ if (!stripeKey) {
+ return next('route');
+ }
+
+ var plans = undefsafe(config, 'payment.stripe.plans');
+ if (!plans) {
+ return next(412, 'Missing stripe plans'); // 412: precondition failed
+ }
+ var metadata = {
+ type: req.body.buyer_type, // jshint ignore:line
+ country: req.body.country,
+ vat: req.body.vat
+ };
+ // Get the credit card details submitted by the form
+ var stripSubscriptionData = {
+ email: req.body.email,
+ card: req.body.stripeToken,
+ metadata: metadata,
+ };
+ if (req.body.coupon) {
+ stripSubscriptionData.coupon = req.body.coupon;
+ }
+ function getPlan(customer) {
+ var yearly = req.body.subscription === 'yearly';
+ var card = customer.cards.default_card; // jshint ignore:line
+ var planOptions = yearly ? plans.yearly : plans.monthly;
+ var EU = ['AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK']; //
+ var country = customer.cards.data.reduce(function (last, current, array) {
+ if (current.id === card) {
+ return current.country;
+ } else {
+ return last;
+ }
+ }, null);
+
+ var p = planOptions.simple;
+
+ // if country === UK - add VAT
+ if (country) {
+ if (country === 'GB') {
+ p = planOptions.vat;
+ } else if (EU.indexOf(country) !== -1) {
+ if (!req.vatValid) {
+ p = planOptions.vat;
+ }
+ }
+ }
+
+ return p;
+ }
+
+ // PROMISE ALL THE THINGS! \o/
+ var getCustomerByUser = Promise.denodeify(customer.getCustomerByUser).bind(customer);
+ var setCustomer = Promise.denodeify(customer.setCustomer).bind(customer);
+ var setProAccount = Promise.denodeify(models.user.setProAccount).bind(models.user);
+
+ new Promise(function (resolve) {
+ if (req.body.vat) {
+ // check country against the country against the card - it should match
+ // if it fails, we swallow and respond
+ var country = (req.body.country || '').toUpperCase();
+ var vat = (req.body.vat||'').replace(/\s/g, '');
+ vatValidator(country, vat, function (error, result) {
+ // IMPORTANT: this is where we are swallowing the promise, and not carrying on
+ if (error || result.valid === false) {
+ metrics.increment('upgrade.fail.vat');
+ req.flash(req.flash.ERROR, 'VAT did not appear to be valid, can you try again or follow up with <a href="mailto:support@jsbin.com?subject=Failed VAT">support</a>.');
+ return upgrade.payment(req, res, next);
+ }
+
+ req.vatValid = true;
+ resolve();
+ });
+ } else {
+ // passthrough
+ resolve();
+ }
+ }).then(function () {
+ // 1. create (or get) customer
+ // 2. update customer with card and details
+ // 3. subscribe to plan (and adjust their VAT based on the country of the card)
+
+ /** 1. get a stripe customer id */
+ if (debug) {console.log('getCustomerByUser');}
+ return getCustomerByUser(req.session.user).then(function (result) {
+ if (debug) {console.log('.then, stripe.customers.retrieve(' + result.stripe_id + ')');} // jshint ignore:line
+ return stripe.customers.retrieve(result.stripe_id).then(function (stripeCustomer) { // jshint ignore:line
+ if (debug) {console.log('.then, subscribe');}
+ // change their subscription
+ // FIXME this *assumes* that the customer is subscribed to a plan...
+ if (stripeCustomer.subscriptions && stripeCustomer.subscriptions.data.length) {
+ return stripe.customers.updateSubscription(stripeCustomer.id, stripeCustomer.subscriptions.data[0].id, { plan: getPlan(stripeCustomer) });
+ } else {
+ return stripe.customers.createSubscription(stripeCustomer.id, { plan: getPlan(stripeCustomer) });
+ }
+ }).catch(function () {
+ // failed to subscribe existing user to stripe
+ metrics.increment('upgrade.fail.existing-user-change-subscription');
+ });
+ }).catch(function () {
+ if (debug) {console.log('.catch, stripe.customers.create(' + JSON.stringify(stripSubscriptionData) + ')');}
+ // create the customer with Stripe since they don't exist
+ return stripe.customers.create(stripSubscriptionData).then(function (stripeCustomer) {
+ if (debug) {console.log('.then, stripe.customers.createSubscription(' + stripeCustomer.id + ', { plan: ' + getPlan(stripeCustomer) + ' })');}
+ return stripe.customers.createSubscription(stripeCustomer.id, { plan: getPlan(stripeCustomer) }).then(function () {
+ if (debug) {console.log('.then, setCustomer');}
+ return setCustomer({
+ stripeId: stripeCustomer.id,
+ user: req.session.user.name,
+ plan: getPlan(stripeCustomer)
+ });
+ });
+ });
+ }).then(function (data) {
+ if (debug) {
+ console.log('.then, setProAccount(' + req.session.user.name + ', true)');
+ console.log(util.inspect(data, { depth: 50 }));
+ console.log('stripe all done - now setting user to pro!');
+ }
+
+ return setProAccount(req.session.user.name, true);
+ }).then(function () {
+ metrics.increment('upgrade.success');
+ req.session.user.pro = true;
+ req.flash(req.flash.NOTIFICATION, 'Welcome to the pro user lounge. Your seat is waiting');
+ res.redirect('/');
+ }).catch(function (error) {
+ // there was something wrong with the customer create process, so let's
+ // send them back to the payment page with a flash message
+ var message = 'Unknown error in the upgrade process. Please try again or contact support';
+ if (error && error.message) {
+ message = error.message;
+ } else if (error) {
+ message = error.toString();
+ }
+ if (error.type) {
+ metrics.increment('upgrade.fail.' + error.type);
+ } else {
+ metrics.increment('upgrade.fail.transaction-misc');
+ }
+
+ req.flash(req.flash.ERROR, message);
+ upgrade.payment(req, res, next);
+ });
+ }).catch(function (error) {
+ // this is likely to be an exception in our code. ![Dangit](http://i.imgur.com/Cj57XRN.gif)
+ console.error('uncaught exception in upgrade', error);
+
+ metrics.increment('upgrade.fail.exception-in-code');
+
+ var message = 'Unknown error in the upgrade process. Please try again or contact support';
+ if (error && error.message) {
+ message = error.message;
+ } else if (error) {
+ message = error.toString();
+ }
+
+ req.flash(req.flash.ERROR, 'Exception in upgrade process: ' + message);
+ upgrade.payment(req, res, next);
+ });
+};
View
@@ -195,6 +195,7 @@ module.exports = function (app) {
app.get('/account/upgrade', features.route('upgrade'), upgradeHandler.features);
app.get('/account/upgrade/pay', features.route('upgrade'), upgradeHandler.payment);
+ app.post('/account/upgrade/pay', features.route('upgrade'), upgradeHandler.processPayment);
// Account settings
var renderAccountSettings = (function(){
@@ -1,7 +1,7 @@
'use strict';
var models = require('../../models');
var sessionStore = require('../../app').sessionStore;
-var Promise = require('rsvp').Promise;
+var Promise = require('rsvp').Promise; // jshint ignore:line
module.exports = function (data, callbackToStripe) {
var sendStripe200 = callbackToStripe.bind(null, null);
@@ -21,7 +21,8 @@ module.exports = function (data, callbackToStripe) {
function isCanceled(obj) {
return obj.status ? obj.status === 'canceled' : isCanceled(obj.data || obj.object);
}
-function removeProStatusFromDB (customer) {
+
+function removeProStatusFromDB(customer) {
return new Promise(function(resolve, reject) {
models.user.setProAccount(customer.user, false, function(err) {
if (err) {
@@ -32,6 +33,6 @@ function removeProStatusFromDB (customer) {
});
}
-function removeProStatusFromSession (customer) {
+function removeProStatusFromSession(customer) {
return sessionStore.set(customer.name, false);
}
Oops, something went wrong.

0 comments on commit 8e74e66

Please sign in to comment.