var nconf = require('nconf'); var moment = require('moment'); var async = require('async'); var _ = require('lodash'); var url = require('url'); var User = require('mongoose').model('User'); var payments = require('./index'); var logger = require('../../logging'); var ipn = require('paypal-ipn'); var paypal = require('paypal-rest-sdk'); var shared = require('habitrpg-shared'); // This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have // a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created // there, get it's plan.id and store it in config.json _.each(shared.content.subscriptionBlocks, function(block){ block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.months); }); paypal.configure({ 'mode': nconf.get("PAYPAL:mode"), //sandbox or live 'client_id': nconf.get("PAYPAL:client_id"), 'client_secret': nconf.get("PAYPAL:client_secret") }); var parseErr = function(res, err){ //var error = err.response ? err.response.message || err.response.details[0].issue : err; var error = JSON.stringify(err); return res.json(400,{err:error}); } exports.createBillingAgreement = function(req,res,next){ req.session.paypalBlock = req.query.sub; var block = shared.content.subscriptionBlocks[req.query.sub]; var billingPlanTitle = "HabitRPG Subscription" + ' ($'+block.price+' every '+block.months+' months, recurring)'; var billingAgreementAttributes = { "name": billingPlanTitle, "description": billingPlanTitle, "start_date": moment().add({minutes:5}).format(), "plan": { "id": block.paypalKey }, "payer": { "payment_method": "paypal" } }; paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) { if (err) return parseErr(res, err); // For approving subscription via Paypal, first redirect user to: approval_url var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href; res.redirect(approval_url); }); } exports.executeBillingAgreement = function(req,res,next){ var block = shared.content.subscriptionBlocks[req.session.paypalBlock]; delete req.session.paypalBlock; async.auto({ exec: function (cb) { paypal.billingAgreement.execute(req.query.token, {}, cb); }, get_user: function (cb) { User.findById(req.session.userId, cb); }, create_sub: ['exec', 'get_user', function (cb, results) { payments.createSubscription({ user: results.get_user, customerId: results.exec.id, paymentMethod: 'Paypal', sub: block }, cb); }] },function(err){ if (err) return parseErr(res, err); res.redirect('/'); }) } exports.createPayment = function(req, res) { // if we're gifting to a user, put it in session for the `execute()` req.session.gift = req.query.gift || undefined; var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; var price = !gift ? 5.00 : gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2) : Number(shared.content.subscriptionBlocks[gift.subscription.months].price).toFixed(2); var description = !gift ? "HabitRPG Gems" : gift.type=='gems' ? "HabitRPG Gems (Gift)" : gift.subscription.months + "mo. HabitRPG Subscription (Gift)"; var create_payment = { "intent": "sale", "payer": { "payment_method": "paypal" }, "redirect_urls": { "return_url": nconf.get('BASE_URL') + '/paypal/checkout/success', "cancel_url": nconf.get('BASE_URL') }, "transactions": [{ "item_list": { "items": [{ "name": description, //"sku": "1", "price": price, "currency": "USD", "quantity": 1 }] }, "amount": { "currency": "USD", "total": price }, "description": description }] }; paypal.payment.create(create_payment, function (err, payment) { if (err) return parseErr(res, err); var link = _.find(payment.links, {rel: 'approval_url'}).href; res.redirect(link); }); } exports.executePayment = function(req, res) { var paymentId = req.query.paymentId, PayerID = req.query.PayerID, gift = req.session.gift ? JSON.parse(req.session.gift) : undefined; delete req.session.gift; async.waterfall([ function(cb){ paypal.payment.execute(paymentId, {payer_id: PayerID}, cb); }, function(payment, cb){ async.parallel([ function(cb2){ User.findById(req.session.userId, cb2); }, function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); } ], cb); }, function(results, cb){ if (_.isEmpty(results[0])) return cb("User not found when completing paypal transaction"); var data = {user:results[0], customerId:PayerID, paymentMethod:'Paypal', gift:gift} var method = 'buyGems'; if (gift) { gift.member = results[1]; if (gift.type=='subscription') method = 'createSubscription'; data.paymentMethod = 'Gift'; } payments[method](data, cb); } ],function(err){ if (err) return parseErr(res, err); res.redirect('/'); }) } exports.cancelSubscription = 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.auto({ get_cus: function(cb){ paypal.billingAgreement.get(user.purchased.plan.customerId, cb); }, verify_cus: ['get_cus', function(cb, results){ var hasntBilledYet = results.get_cus.agreement_details.cycles_completed == "0"; if (hasntBilledYet) return cb("The plan hasn't activated yet (due to a PayPal bug). It will begin "+results.get_cus.agreement_details.next_billing_date+", after which you can cancel to retain your full benefits"); cb(); }], del_cus: ['verify_cus', function(cb, results){ paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb); }], cancel_sub: ['get_cus', 'verify_cus', function(cb, results){ var data = {user: user, paymentMethod: 'Paypal', nextBill: results.get_cus.agreement_details.next_billing_date}; payments.cancelSubscription(data, cb) }] }, function(err){ if (err) return parseErr(res, err); res.redirect('/'); user = null; }); } /** * General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their * recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution */ exports.ipn = function(req, res, next) { console.log('IPN Called'); res.send(200); // Must respond to PayPal IPN request with an empty 200 first ipn.verify(req.body, function(err, msg) { if (err) return logger.error(msg); switch (req.body.txn_type) { // TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead... case 'recurring_payment_profile_cancel': case 'subscr_cancel': User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){ if (err) return logger.error(err); if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel) payments.cancelSubscription({user:user, paymentMethod: 'Paypal'}); }); break; } }); };