diff --git a/package.json b/package.json index 20dd2b1602..e9e537cba2 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "domain-middleware": "~0.1.0", "universal-analytics": "~0.3.2", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", - "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca" + "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", + "paypal-ipn": "~1.0.1" }, "private": true, "subdomain": "habitrpg", diff --git a/src/controllers/payments.js b/src/controllers/payments.js new file mode 100644 index 0000000000..a5c69f3116 --- /dev/null +++ b/src/controllers/payments.js @@ -0,0 +1,215 @@ +/* @see ./routes.coffee for routing*/ + +var _ = require('lodash'); +var logger = require('../logging'); +var ipn = require('paypal-ipn'); +var shared = require('habitrpg-shared'); +var nconf = require('nconf'); +var async = require('async'); +var User = require('./../models/user').model; +var ga = require('./../utils').ga; +var logging = require('./../logging'); +var userAPI = require('./user'); +var api = module.exports; +var isProduction = nconf.get("NODE_ENV") === "production"; + +var PaypalRecurring = require('paypal-recurring'); +var paypalRecurring = new PaypalRecurring({ + username: nconf.get('PAYPAL_USERNAME'), + password: nconf.get('PAYPAL_PASSWORD'), + signature: nconf.get('PAYPAL_SIGNATURE') +}, isProduction ? "production" : "sandbox"); +var paypalCheckout = require('paypal-express-checkout') + .init(nconf.get('PAYPAL_USERNAME'), nconf.get('PAYPAL_PASSWORD'), nconf.get('PAYPAL_SIGNATURE'), nconf.get('BASE_URL'), nconf.get('BASE_URL'), !isProduction); + +function revealMysteryItems(user) { + _.each(shared.content.gear.flat, function(item) { + if ( + item.klass === 'mystery' && + moment().isAfter(item.mystery.start) && + moment().isBefore(item.mystery.end) && + !user.items.gear.owned[item.key] && + !~user.purchased.plan.mysteryItems.indexOf(item.key) + ) { + user.purchased.plan.mysteryItems.push(item.key); + } + }); +} + +function createSubscription(user, data) { + if (!user.purchased.plan) user.purchased.plan = {}; + _(user.purchased.plan) + .merge({ // override with these values + planId:'basic_earned', + customerId: data.customerId, + dateUpdated: new Date, + gemsBought: 0, + paymentMethod: data.paymentMethod + }) + .defaults({ // allow non-override if a plan was previously used + dateCreated: new Date, + mysteryItems: [] + }); + revealMysteryItems(user); + user.purchased.txnCount++; + ga.event('subscribe', data.paymentMethod).send() + ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); +} + +function cancelSubscription(user, data){ + _.merge(user.purchased.plan, {planId:null, customerId:null, paymentMethod:null}); + user.markModified('purchased.plan'); + ga.event('unsubscribe', 'Stripe').send(); +} + +function buyGems(user, data) { + user.balance += 5; + user.purchased.txnCount++; + ga.event('checkout', data.paymentMethod).send(); + ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); +} + +/* + Setup Stripe response when posting payment + */ +api.stripeCheckout = function(req, res, next) { + var api_key = nconf.get('STRIPE_API_KEY'); + var stripe = require("stripe")(api_key); + var token = req.body.id; + var user = res.locals.user; + + async.waterfall([ + function(cb){ + if (req.query.plan) { + stripe.customers.create({ + email: req.body.email, + metadata: {uuid: res.locals.user._id}, + card: token, + plan: req.query.plan, + }, cb); + } else { + stripe.charges.create({ + amount: "500", // $5 + currency: "usd", + card: token + }, cb); + } + }, + function(response, cb) { + if (req.query.plan) { + createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'}); + } else { + buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'}); + } + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.send(200); + }); +}; + +api.stripeSubscribeCancel = function(req, res, next) { + var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); + var user = res.locals.user; + if (!user.purchased.plan.customerId) + return res.json(401, {err: "User does not have a plan subscription"}); + + async.waterfall([ + function(cb) { + stripe.customers.del(user.purchased.plan.customerId, cb); + }, + function(response, cb) { + cancelSubscription(user); + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.redirect('/'); + }); +} + +api.paypalSubscribe = function(req,res,next) { + var uuid = res.locals.user._id; + // Authenticate a future subscription of ~5 USD + paypalRecurring.authenticate({ + RETURNURL: nconf.get('BASE_URL') + '/paypal/subscribe/success?uuid=' + uuid, + CANCELURL: nconf.get("BASE_URL"), + PAYMENTREQUEST_0_AMT: 5, + L_BILLINGAGREEMENTDESCRIPTION0: "HabitRPG Subscription" + }, function(err, data, url) { + // Redirect the user if everything went well with + // a HTTP 302 according to PayPal's guidelines + if (err) return next(err); + res.redirect(302, url); + }); +} + +api.paypalSubscribeSuccess = function(req,res,next) { + // Create a subscription of 10 USD every month + var uuid = req.query.uuid; + if (!uuid) return next("UUID required"); + paypalRecurring.createSubscription(req.query.token, req.query.PayerID,{ + AMT: 5, + DESC: "HabitRPG Subscription", + BILLINGPERIOD: "Month", + BILLINGFREQUENCY: 1, + }, function(err, data) { + if (err) return res.next(err); + User.findById(uuid, function(err,user){ + if (err) return next(err); + createSubscription(user, {customerId: data.PROFILEID, paymentMethod: 'Paypal'}); + user.save(function(err,saved){ + res.redirect('/'); + }) + }) + }); +} + +api.paypalSubscribeCancel = function(req, res, next) { + var user = res.locals.user; + if (!user.purchased.plan.customerId) + return res.json(401, {err: "User does not have a plan subscription"}); + async.waterfall([ + function(cb) { + paypalRecurring.modifySubscription(user.purchased.plan.customerId, 'cancel', cb); + }, + function(response, cb) { + cancelSubscription(user); + user.save(cb); + } + ], function(err, saved){ + if (err) return next(err); + res.redirect('/'); + }); + +} + +api.paypalCheckout = function(req, res, next) { + var uuid = res.locals.user._id; + var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + uuid}; + paypalCheckout.pay(+new Date, 5, 'HabitRPG Gems', 'USD', opts, function(err, url) { + if (err) return next(err); + res.redirect(url); + }); +} + +api.paypalCheckoutSuccess = function(req,res,next) { + paypalCheckout.detail(req.query.token, req.query.PayerID, function(err, data, invoiceNumber, price) { + // see `data` vars at https://github.com/petersirka/node-paypal-express-checkout#paypal-account + //if (err) return next('PayPal Error: ' + msg); + if (err) return next(err); + if (data.ACK !== 'Success') return next('PayPal transaction failed, please try again'); + + var uuid = req.query.uuid; //, apiToken = query.apiToken; + User.findById(uuid , function(err, user) { + if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction"; + if (err) return next(err); + buyGems(user, {customerId:req.query.PayerID, paymentMethod:'Paypal'}); + user.save(function(){ + if (err) return next(err); + res.redirect('/'); + }); + }); + }); +} \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index 2296697e6d..91329c4a2b 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -268,18 +268,12 @@ api['delete'] = function(req, res, next) { /* ------------------------------------------------------------------------ - Unlock Preferences + Gems ------------------------------------------------------------------------ */ // api.unlock // see Shared.ops -/* - ------------------------------------------------------------------------ - Buy Gems - ------------------------------------------------------------------------ - */ - api.addTenGems = function(req, res, next) { var user = res.locals.user; user.balance += 2.5; @@ -289,200 +283,6 @@ api.addTenGems = function(req, res, next) { }) } -// TODO delete plan - -function revealMysteryItems(user) { - _.each(shared.content.gear.flat, function(item) { - if ( - item.klass === 'mystery' && - moment().isAfter(item.mystery.start) && - moment().isBefore(item.mystery.end) && - !user.items.gear.owned[item.key] && - !~user.purchased.plan.mysteryItems.indexOf(item.key) - ) { - user.purchased.plan.mysteryItems.push(item.key); - } - }); -} - -function createSubscription(user, data) { - if (!user.purchased.plan) user.purchased.plan = {}; - _(user.purchased.plan) - .merge({ // override with these values - planId:'basic_earned', - customerId: data.customerId, - dateUpdated: new Date, - gemsBought: 0, - paymentMethod: data.paymentMethod - }) - .defaults({ // allow non-override if a plan was previously used - dateCreated: new Date, - mysteryItems: [] - }); - revealMysteryItems(user); - user.purchased.txnCount++; - ga.event('subscribe', data.paymentMethod).send() - ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); -} - -function cancelSubscription(user, data){ - _.merge(user.purchased.plan, {planId:null, customerId:null, paymentMethod:null}); - user.markModified('purchased.plan'); - ga.event('unsubscribe', 'Stripe').send(); -} - -function buyGems(user, data) { - user.balance += 5; - user.purchased.txnCount++; - ga.event('checkout', data.paymentMethod).send(); - ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); -} - -/* - Setup Stripe response when posting payment - */ -api.stripeCheckout = function(req, res, next) { - var api_key = nconf.get('STRIPE_API_KEY'); - var stripe = require("stripe")(api_key); - var token = req.body.id; - var user = res.locals.user; - - async.waterfall([ - function(cb){ - if (req.query.plan) { - stripe.customers.create({ - email: req.body.email, - metadata: {uuid: res.locals.user._id}, - card: token, - plan: req.query.plan, - }, cb); - } else { - stripe.charges.create({ - amount: "500", // $5 - currency: "usd", - card: token - }, cb); - } - }, - function(response, cb) { - if (req.query.plan) { - createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'}); - } else { - buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'}); - } - user.save(cb); - } - ], function(err, saved){ - if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors - res.send(200); - }); -}; - -api.stripeSubscribeCancel = function(req, res, next) { - var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); - var user = res.locals.user; - if (!user.purchased.plan.customerId) - return res.json(401, {err: "User does not have a plan subscription"}); - - async.waterfall([ - function(cb) { - stripe.customers.del(user.purchased.plan.customerId, cb); - }, - function(response, cb) { - cancelSubscription(user); - user.save(cb); - } - ], function(err, saved){ - if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors - res.redirect('/'); - }); -} - -api.paypalSubscribe = function(req,res,next) { - var uuid = res.locals.user._id; - // Authenticate a future subscription of ~5 USD - paypalRecurring.authenticate({ - RETURNURL: nconf.get('BASE_URL') + '/paypal/subscribe/success?uuid=' + uuid, - CANCELURL: nconf.get("BASE_URL"), - PAYMENTREQUEST_0_AMT: 5, - L_BILLINGAGREEMENTDESCRIPTION0: "HabitRPG Subscription" - }, function(err, data, url) { - // Redirect the user if everything went well with - // a HTTP 302 according to PayPal's guidelines - if (err) return next(err); - res.redirect(302, url); - }); -} - -api.paypalSubscribeSuccess = function(req,res,next) { - // Create a subscription of 10 USD every month - var uuid = req.query.uuid; - if (!uuid) return next("UUID required"); - paypalRecurring.createSubscription(req.query.token, req.query.PayerID,{ - AMT: 5, - DESC: "HabitRPG Subscription", - BILLINGPERIOD: "Month", - BILLINGFREQUENCY: 1, - }, function(err, data) { - if (err) return res.next(err); - User.findById(uuid, function(err,user){ - if (err) return next(err); - createSubscription(user, {customerId: data.PROFILEID, paymentMethod: 'Paypal'}); - user.save(function(err,saved){ - res.redirect('/'); - }) - }) - }); -} - -api.paypalSubscribeCancel = function(req, res, next) { - var user = res.locals.user; - if (!user.purchased.plan.customerId) - return res.json(401, {err: "User does not have a plan subscription"}); - async.waterfall([ - function(cb) { - paypalRecurring.modifySubscription(user.purchased.plan.customerId, 'cancel', cb); - }, - function(response, cb) { - cancelSubscription(user); - user.save(cb); - } - ], function(err, saved){ - if (err) return next(err); - res.redirect('/'); - }); - -} - -api.paypalCheckout = function(req, res, next) { - var uuid = res.locals.user._id; - var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + uuid}; - paypalCheckout.pay(+new Date, 5, 'HabitRPG Gems', 'USD', opts, function(err, url) { - if (err) return next(err); - res.redirect(url); - }); -} - -api.paypalCheckoutSuccess = function(req,res,next) { - paypalCheckout.detail(req.query.token, req.query.PayerID, function(err, data, invoiceNumber, price) { - // see `data` vars at https://github.com/petersirka/node-paypal-express-checkout#paypal-account - //if (err) return next('PayPal Error: ' + msg); - if (err) return next(err); - if (data.ACK !== 'Success') return next('PayPal transaction failed, please try again'); - - var uuid = req.query.uuid; //, apiToken = query.apiToken; - User.findById(uuid , function(err, user) { - if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction"; - if (err) return next(err); - buyGems(user, {customerId:req.query.PayerID, paymentMethod:'Paypal'}); - user.save(function(){ - if (err) return next(err); - res.redirect('/'); - }); - }); - }); -} - /* ------------------------------------------------------------------------ Tags diff --git a/src/routes/pages.js b/src/routes/pages.js index 555aa7a157..80e7be54e1 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -57,16 +57,4 @@ router.get('/static/extensions', function(req, res) { res.redirect('http://habitrpg.wikia.com/wiki/App_and_Extension_Integrations'); }); -// --------- PayPal -------- - -router.get('/paypal/checkout', auth.authWithUrl, user.paypalCheckout); -router.get('/paypal/checkout/success', user.paypalCheckoutSuccess); -router.get('/paypal/subscribe', auth.authWithUrl, user.paypalSubscribe); -router.get('/paypal/subscribe/success', user.paypalSubscribeSuccess); -router.get('/paypal/subscribe/cancel', auth.authWithUrl, user.paypalSubscribeCancel); - -router.post("/stripe/checkout", auth.auth, user.stripeCheckout); -//router.get("/stripe/subscribe", auth.authWithUrl, user.stripeSubscribe); // checkout route is used (above) with ?plan= instead -router.get("/stripe/subscribe/cancel", auth.authWithUrl, user.stripeSubscribeCancel); - module.exports = router; \ No newline at end of file diff --git a/src/routes/payments.js b/src/routes/payments.js new file mode 100644 index 0000000000..1109f85c5c --- /dev/null +++ b/src/routes/payments.js @@ -0,0 +1,17 @@ +var nconf = require('nconf'); +var express = require('express'); +var router = new express.Router(); +var auth = require('../controllers/auth'); +var payments = require('../controllers/payments'); + +router.get('/paypal/checkout', auth.authWithUrl, payments.paypalCheckout); +router.get('/paypal/checkout/success', payments.paypalCheckoutSuccess); +router.get('/paypal/subscribe', auth.authWithUrl, payments.paypalSubscribe); +router.get('/paypal/subscribe/success', payments.paypalSubscribeSuccess); +router.get('/paypal/subscribe/cancel', auth.authWithUrl, payments.paypalSubscribeCancel); + +router.post("/stripe/checkout", auth.auth, payments.stripeCheckout); +//router.get("/stripe/subscribe", auth.authWithUrl, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead +router.get("/stripe/subscribe/cancel", auth.authWithUrl, payments.stripeSubscribeCancel); + +module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index bef7642207..1a48dbf5b2 100644 --- a/src/server.js +++ b/src/server.js @@ -123,6 +123,7 @@ if (cluster.isMaster && (isDev || isProd)) { // Custom Directives app.use(require('./routes/pages').middleware); + app.use(require('./routes/payments').middleware); app.use(require('./routes/auth').middleware); var v2 = express(); app.use('/api/v2', v2);