feat(gifts): allow gifting gems from paypal/stripe

This commit is contained in:
Tyler Renelle 2014-11-26 15:14:23 -07:00
parent 7e9b92fcad
commit e8dbd5ca1a
10 changed files with 85 additions and 52 deletions

View file

@ -2,7 +2,7 @@
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
Members.selectMember('9', function(){
Members.selectMember('d7f809df-81bd-4d58-992b-851d94bfaccb', function(){
$rootScope.openModal('send-gift',{controller:'MemberModalCtrl'})
});

View file

@ -3,8 +3,8 @@
/* Make user and settings available for everyone through root scope.
*/
habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content', '$modal', '$timeout', 'ApiUrlService',
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrlService) {
habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content', '$modal', '$timeout', 'ApiUrlService', '$filter',
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrlService, $filter) {
var user = User.user;
var initSticky = _.once(function(){
@ -139,24 +139,30 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$rootScope.flash[type].splice($index, 1);
}
$rootScope.showStripe = function(subscription) {
$rootScope.encodeGift = function(uuid, gift){
return JSON.stringify({uuid:uuid, amount:gift.gems.amount, message:gift.message});
}
$rootScope.showStripe = function(data) {
StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY,
address: false,
amount: 500,
name: subscription ? window.env.t('subscribe') : window.env.t('checkout'),
description: subscription ?
amount: data.gift ? data.gift.gems.amount/4*100 : 500,
name: data.subscription ? window.env.t('subscribe') : window.env.t('checkout'),
description: data.subscription ?
window.env.t('buySubsText') :
window.env.t('donationDesc'),
panelLabel: subscription ? window.env.t('subscribe') : window.env.t('checkout'),
token: function(data) {
panelLabel: data.subscription ? window.env.t('subscribe') : window.env.t('checkout'),
token: function(res) {
var url = '/stripe/checkout';
if (subscription) url += '?plan=basic_earned';
if (data.gift) url += '?gift=' + $rootScope.encodeGift(data.uuid, data.gift);
//TODO else if? ^
if (data.subscription) url += '?plan=basic_earned';
$scope.$apply(function(){
$http.post(url, data).success(function() {
$http.post(url, res).success(function() {
window.location.reload(true);
}).error(function(data) {
alert(data.err);
}).error(function(res) {
alert(res.err);
});
})
}

View file

@ -29,15 +29,16 @@ api.getMember = function(req, res, next) {
})
}
var sendMessage = function(user, member, msg){
var message = groups.chatDefaults(msg, user);
shared.refPush(member.inbox.messages, message);
api.sendMessage = function(user, member, data){
var msg = data.amount
? "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you " + data.amount + " gems!` " + data.message
: data.message;
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));
member.inbox.newMessages++;
member._v++;
member.markModified('inbox.messages');
var message = groups.chatDefaults(msg, member);
shared.refPush(user.inbox.messages, _.defaults({sent:true},message));
shared.refPush(user.inbox.messages, _.defaults({sent:true}, groups.chatDefaults(msg, member)));
user.markModified('inbox.messages');
}
@ -50,7 +51,7 @@ api.sendPrivateMessage = function(req, res, next){
|| member.inbox.optOut) { // or if they've opted out of messaging
return cb({code: 401, err: "Can't send message to this user."});
}
sendMessage(res.locals.user, member, req.body.message);
api.sendMessage(res.locals.user, member, {message:req.body.message});
async.parallel([
function (cb2) { member.save(cb2) },
function (cb2) { res.locals.user.save(cb2) }
@ -75,12 +76,10 @@ api.sendGift = function(req, res, next){
return cb({code: 401, err: "Amount must be within 0 and your current number of gems."});
member.balance += amt;
user.balance -= amt;
var msg = "`Hello "+member.profile.name+", "+user.profile.name+" has sent you "+amt+" gems!` ";
if (req.body.message) msg += req.body.message;
sendMessage(user, member, msg);
api.sendMessage(user, member, {amount:req.body.gems.amount, message:req.body.message});
return async.parallel([
function (cb2) { member.save(cb2) },
function (cb2) { res.locals.user.save(cb2) }
function (cb2) { user.save(cb2) }
], cb);
case "subscription":
return cb();

View file

@ -7,6 +7,9 @@ var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe');
var paypal = require('./paypal');
var User = require('mongoose').model('User');
var members = require('../members')
var async = require('async');
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
@ -61,12 +64,21 @@ exports.cancelSubscription = function(user, data) {
utils.ga.event('unsubscribe', 'Stripe').send();
}
exports.buyGems = function(user, data) {
user.balance += 5;
user.purchased.txnCount++;
if(isProduction) utils.txnEmail(user, 'donation');
exports.buyGems = function(data, cb) {
var amt = data.gift ? data.gift.amount/4 : 5;
(data.gift ? data.gift.member : data.user).balance += amt;
data.user.purchased.txnCount++;
if(isProduction) {
utils.txnEmail(data.user, 'donation');
utils.ga.event('checkout', data.paymentMethod).send();
utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
//TODO ga.transaction to reflect whether this is gift or self-purchase
utils.ga.transaction(data.customerId, amt).item(amt, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
}
if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift);
async.parallel([
function(cb2){data.user.save(cb2)},
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
], cb);
}
exports.stripeCheckout = stripe.checkout;

View file

@ -3,7 +3,7 @@ var moment = require('moment');
var async = require('async');
var _ = require('lodash');
var url = require('url');
var mongoose = require('mongoose');
var User = require('mongoose').model('User');
var payments = require('./index');
var logger = require('../../logging');
var ipn = require('paypal-ipn');
@ -51,7 +51,7 @@ exports.executeBillingAgreement = function(req,res,next){
paypal.billingAgreement.execute(req.query.token, {}, cb);
},
function(billingAgreement, cb){
mongoose.model('User').findById(req.session.userId, function(err, user){
User.findById(req.session.userId, function(err, user){
if (err) return cb(err);
cb(null, {billingAgreement:billingAgreement, user:user});
});
@ -67,6 +67,9 @@ exports.executeBillingAgreement = function(req,res,next){
}
exports.createPayment = function(req, res, next) {
// if we're gifting to a user, put it in session for the `execute()`
req.session.gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
var price = req.session.gift ? Number(req.session.gift.amount/4).toFixed(2) : 5.00;
var create_payment = {
"intent": "sale",
"payer": {
@ -81,14 +84,14 @@ exports.createPayment = function(req, res, next) {
"items": [{
"name": "HabitRPG Gems",
//"sku": "1",
"price": "5.00",
"price": price,
"currency": "USD",
"quantity": 1
}]
},
"amount": {
"currency": "USD",
"total": "5.00"
"total": price
},
"description": "HabitRPG Gems"
}]
@ -102,20 +105,25 @@ exports.createPayment = function(req, res, next) {
exports.executePayment = function(req, res, next) {
var paymentId = req.query.paymentId,
PayerID = req.query.PayerID;
PayerID = req.query.PayerID,
gift = req.session.gift;
async.waterfall([
function(cb){
paypal.payment.execute(paymentId, {payer_id: PayerID}, cb);
},
function(payment, cb){
mongoose.model('User').findById(req.session.userId, cb);
async.parallel([
function(cb2){ User.findById(req.session.userId, cb2); },
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); }
], cb);
},
function(user, cb){
if (_.isEmpty(user)) return cb("user not found when completing paypal transaction");
payments.buyGems(user, {customerId:PayerID, paymentMethod:'Paypal'});
user.save(cb);
function(results, cb){
if (_.isEmpty(results[0]))
return cb("user not found when completing paypal transaction");
if (gift) gift.member = results[1];
payments.buyGems({user:results[0], customerId:PayerID, paymentMethod:'Paypal', gift:gift}, cb);
}
],function(err, saved){
],function(err, results){
if (err) return next(parseErr(err));
res.redirect('/');
})
@ -153,7 +161,7 @@ exports.ipn = function(req, res, next) {
// 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':
mongoose.model('User').findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
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);

View file

@ -2,6 +2,7 @@ var nconf = require('nconf');
var stripe = require("stripe")(nconf.get('STRIPE_API_KEY'));
var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
/*
Setup Stripe response when posting payment
@ -9,19 +10,20 @@ var payments = require('./index');
exports.checkout = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
async.waterfall([
function(cb){
if (req.query.plan) {
stripe.customers.create({
email: req.body.email,
metadata: {uuid: res.locals.user._id},
metadata: {uuid: user._id},
card: token,
plan: req.query.plan
}, cb);
} else {
stripe.charges.create({
amount: "500", // $5
amount: gift ? ""+gift.amount/4*100 : "500", //"500" = $5
currency: "usd",
card: token
}, cb);
@ -30,10 +32,16 @@ exports.checkout = function(req, res, next) {
function(response, cb) {
if (req.query.plan) {
payments.createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'});
} else {
payments.buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'});
}
user.save(cb);
} else {
async.waterfall([
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) },
function(member, cb2){
if (gift) gift.member = member;
payments.buyGems({user:user, customerId:response.id, paymentMethod:'Stripe', gift:gift},cb2);
}
], cb);
}
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors

View file

@ -93,10 +93,10 @@ GroupSchema.methods.toJSON = function(){
return doc;
}
var chatDefaults = module.exports.chatDefaults = function(message,user){
var chatDefaults = module.exports.chatDefaults = function(msg,user){
var message = {
id: shared.uuid(),
text: message,
text: msg,
timestamp: +new Date,
likes: {}
};
@ -105,7 +105,7 @@ var chatDefaults = module.exports.chatDefaults = function(message,user){
uuid: user._id,
contributor: user.contributor && user.contributor.toObject(),
backer: user.backer && user.backer.toObject(),
user: user.profile.name,
user: user.profile.name
});
} else {
message.uuid = 'system';

View file

@ -237,7 +237,7 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
div(ng-if='!p.customerId || (p.customerId && p.dateTerminated)')
h3(ng-if='(p.customerId && p.dateTerminated)') Resubscribe
a.btn.btn-primary(ng-click='showStripe(true)') Card
a.btn.btn-primary(ng-click='showStripe({subscription:true})') Card
//a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal
div(ng-if='p.customerId')

View file

@ -20,7 +20,7 @@ script(id='modals/buyGems.html', type='text/ng-template')
td
p
small.muted=env.t('paymentMethods')
.btn.btn-primary(ng-click='showStripe()')=env.t('card')
.btn.btn-primary(ng-click='showStripe({})')=env.t('card')
a.btn.btn-warning(href='/paypal/checkout?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal
div(ng-include="'partials/options.settings.subscription.html'")

View file

@ -96,6 +96,6 @@ script(type='text/ng-template', id='modals/send-gift.html')
.modal-footer
- var fromBal = "gift.type=='gems' && gift.gems.fromBalance"
button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send")
a.btn.btn-primary(ng-hide=fromBal, ng-click='showStripe({uuid:profile._id})')=env.t('card')
a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?uuid={{profile._id}}') PayPal
a.btn.btn-primary(ng-hide=fromBal, ng-click='showStripe({gift:gift, uuid:profile._id})')=env.t('card')
a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{encodeGift(profile._id, gift)}}') PayPal
button.btn.btn-default(ng-click='$close()')=env.t('cancel')