205 lines
7.4 KiB
JavaScript
205 lines
7.4 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
};
|
|
|