diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 7fc249754e..4b30f7f6ec 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -145,21 +145,26 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ } $rootScope.showStripe = function(data) { - var isSub = data.subscription || (data.gift && data.gift.type=='subscription'); + var sub = + data.subscription ? data.subscription + : data.gift && data.gift.type=='subscription' ? data.gift.subscription.months + : false; + sub = sub && Content.subscriptionBlocks[sub]; + var amount = // 500 = $5 + sub ? sub.price*100 + : data.gift && data.gift.type=='gems' ? data.gift.gems.amount/4*100 + : 500; StripeCheckout.open({ key: window.env.STRIPE_PUB_KEY, address: false, - amount: !data.gift ? 500 : // 500 = $5 - data.gift.type=='subscription' ? Content.subscriptionBlocks[data.gift.subscription.months].price*100: - data.gift.gems.amount/4*100, - name: isSub ? window.env.t('subscribe') : window.env.t('checkout'), - description: isSub ? window.env.t('buySubsText') : window.env.t('donationDesc'), - panelLabel: isSub ? window.env.t('subscribe') : window.env.t('checkout'), + amount: amount, + name: sub ? window.env.t('subscribe') : window.env.t('checkout'), + description: sub ? window.env.t('buySubsText') : window.env.t('donationDesc'), + panelLabel: sub ? window.env.t('subscribe') : window.env.t('checkout'), token: function(res) { - var url = '/stripe/checkout'; - if (data.gift) url += '?gift=' + $rootScope.encodeGift(data.uuid, data.gift); - //TODO else if? ^ - if (data.subscription) url += '?plan=basic_earned'; + var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below + if (data.gift) url += '&gift=' + $rootScope.encodeGift(data.uuid, data.gift); + if (data.subscription) url += '&sub='+sub.months; $scope.$apply(function(){ $http.post(url, res).success(function() { window.location.reload(true); diff --git a/src/controllers/members.js b/src/controllers/members.js index e0a8b653b9..b26ddbabf5 100644 --- a/src/controllers/members.js +++ b/src/controllers/members.js @@ -77,7 +77,7 @@ api.sendGift = function(req, res, next){ case "gems": var amt = req.body.gems.amount / 4, user = res.locals.user; - if (amt < 1 || user.balance < amt) + if (!amt || amt < 1 || user.balance < amt) return cb({code: 401, err: "Amount must be within 0 and your current number of gems."}); member.balance += amt; user.balance -= amt; diff --git a/src/controllers/payments/index.js b/src/controllers/payments/index.js index c20a90a6f3..df8f762382 100644 --- a/src/controllers/payments/index.js +++ b/src/controllers/payments/index.js @@ -27,38 +27,47 @@ function revealMysteryItems(user) { exports.createSubscription = function(data, cb) { var recipient = data.gift ? data.gift.member : data.user; - if (!recipient.purchased.plan) recipient.purchased.plan = {}; + //if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case var p = recipient.purchased.plan; - var giftMonths = data.gift ? data.gift.subscription.months : 0 - _(p).merge({ // override with these values - planId:'basic_earned', - customerId: data.customerId, - dateUpdated: new Date(), - gemsBought: 0, - paymentMethod: data.paymentMethod, - extraMonths: +p.extraMonths - + +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0) - + +(giftMonths), - dateTerminated: null - }).defaults({ // allow non-override if a plan was previously used - dateCreated: new Date(), - mysteryItems: [] - }); + var months = data.gift ? data.gift.subscription.months : data.sub.months; + var block = shared.content.subscriptionBlocks[months]; - // Block sub perks - if (giftMonths) { - p.consecutive.offset += giftMonths; - p.consecutive.gemCapExtra += giftMonths*5; - if (p.consecutive.gemCapExtra > 25) p.consecutive.gemCapExtra = 25; - p.consecutive.trinkets += giftMonths; + if (data.gift) { + if (!p.customerId) p.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId + if (p.dateTerminated) { // User already has a plan + p.dateTerminated = moment(p.dateTerminated).add({months: months}).toDate(); + } else { + p.extraMonths += +months; + } + } else { + _(p).merge({ // override with these values + planId: block.key, + customerId: data.customerId, + dateUpdated: new Date(), + gemsBought: 0, + paymentMethod: data.paymentMethod, + extraMonths: +p.extraMonths + + +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0), + dateTerminated: null + }).defaults({ // allow non-override if a plan was previously used + dateCreated: new Date(), + mysteryItems: [] + }); } + // Block sub perks + var perks = Math.floor(months/3); + if (perks) { + p.consecutive.offset += months; + p.consecutive.gemCapExtra += perks*5; + if (p.consecutive.gemCapExtra > 25) p.consecutive.gemCapExtra = 25; + p.consecutive.trinkets += perks; + } revealMysteryItems(recipient); if(isProduction) { - utils.txnEmail(data.user, 'subscription-begins'); - //TODO proper ga.event / transaction + data.gift && utils.txnEmail(data.user, 'subscription-begins'); utils.ga.event('subscribe', data.paymentMethod).send(); - utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); + utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send(); } data.user.purchased.txnCount++; if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift); @@ -72,16 +81,15 @@ exports.createSubscription = function(data, cb) { * Sets their subscription to be cancelled later */ exports.cancelSubscription = function(user, data) { - var p = user.purchased.plan, - now = moment(); - if(isProduction) utils.txnEmail(user, 'cancel-subscription'); + var p = user.purchased.plan, now = moment(); p.dateTerminated = moment( now.format('MM') + '/' + moment(p.dateUpdated).format('DD') + '/' + now.format('YYYY') ) - .add({months:1})// end their subscription 1mo from their last payment - .add({months:p.extraMonths})// plus any extra time (carry-over, gifted subscription, etc) they have + .add({months:1}) // end their subscription 1mo from their last payment + .add({months: Math.ceil(p.extraMonths)})// plus any extra time (carry-over, gifted subscription, etc) they have. FIXME: moment can't add months in fractions... .toDate(); p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated + if(isProduction) utils.txnEmail(user, 'cancel-subscription'); utils.ga.event('unsubscribe', 'Stripe').send(); } diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js index 1277b7c967..35ca4cfe41 100644 --- a/src/controllers/payments/paypal.js +++ b/src/controllers/payments/paypal.js @@ -13,7 +13,9 @@ 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 -var billingPlanID = nconf.get('PAYPAL:billing_plan_id'); +_.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 @@ -26,13 +28,14 @@ var parseErr = function(err){ } exports.createBillingAgreement = function(req,res,next){ - var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; + var block = req.session.paypalBlock = 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({seconds:5}).format(), "plan": { - "id": billingPlanID + "id": block.paypalKey }, "payer": { "payment_method": "paypal" @@ -58,7 +61,7 @@ exports.executeBillingAgreement = function(req,res,next){ }); }, function(data, cb){ - payments.createSubscription(data.user, {customerId: data.billingAgreement.id, paymentMethod: 'Paypal'}); + payments.createSubscription({user:data.user, customerId: data.billingAgreement.id, paymentMethod: 'Paypal', sub:req.session.paypalBlock}); data.user.save(cb); } ],function(err){ diff --git a/src/controllers/payments/paypalBillingSetup.js b/src/controllers/payments/paypalBillingSetup.js index d4d7f8e167..8c7a1523c0 100644 --- a/src/controllers/payments/paypalBillingSetup.js +++ b/src/controllers/payments/paypalBillingSetup.js @@ -2,12 +2,14 @@ // payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this // file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json), // and once for any time you need to edit the plan thereafter - +require('coffee-script'); var path = require('path'); var nconf = require('nconf'); +_ = require('lodash'); nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json'))); var paypal = require('paypal-rest-sdk'); -var OP = "list"; // list create update remove +var OP = "create"; // list create update remove +var blocks = require('habitrpg-shared').content.subscriptionBlocks; paypal.configure({ 'mode': nconf.get("PAYPAL:mode"), //sandbox or live @@ -15,8 +17,8 @@ paypal.configure({ 'client_secret': nconf.get("PAYPAL:client_secret") }); -var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; // https://developer.paypal.com/docs/api/#billing-plans-and-agreements +var billingPlanTitle ="HabitRPG Subscription"; var billingPlanAttributes = { "name": billingPlanTitle, "description": billingPlanTitle, @@ -26,18 +28,23 @@ var billingPlanAttributes = { "cancel_url": nconf.get("BASE_URL"), "return_url": nconf.get('BASE_URL') + '/paypal/subscribe/success' }, - "payment_definitions": [{ - "name": billingPlanTitle, + payment_definitions: [{ "type": "REGULAR", - "frequency_interval": "1", "frequency": "MONTH", - "cycles": "0", - "amount": { - "currency": "USD", - "value": "5" - } + "cycles": "0" }] }; +_.each(blocks, function(block){ + block.definition = _.cloneDeep(billingPlanAttributes); + _.merge(block.definition.payment_definitions[0], { + "name": billingPlanTitle + ' ($'+block.price+' every '+block.months+' months, recurring)', + "frequency_interval": ""+block.months, + "amount": { + "currency": "USD", + "value": ""+block.price + } + }); +}) switch(OP) { case "list": @@ -48,7 +55,8 @@ switch(OP) { case "update": break; case "create": - paypal.billingPlan.create(billingPlanAttributes, function(err,plan){ + paypal.billingPlan.create(blocks["3"].definition, function(err,plan){ + if (err) return console.log(err); if (plan.state == "ACTIVE") return console.log({err:err, plan:plan}); var billingPlanUpdateAttributes = [{ diff --git a/src/controllers/payments/stripe.js b/src/controllers/payments/stripe.js index 549c58bfb0..edee9501d7 100644 --- a/src/controllers/payments/stripe.js +++ b/src/controllers/payments/stripe.js @@ -12,15 +12,16 @@ 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; + var sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false; async.waterfall([ function(cb){ - if (req.query.plan) { + if (sub) { stripe.customers.create({ email: req.body.email, metadata: {uuid: user._id}, card: token, - plan: req.query.plan + plan: sub.key }, cb); } else { stripe.charges.create({ @@ -33,8 +34,7 @@ exports.checkout = function(req, res, next) { } }, function(response, cb) { - if (req.query.plan) - return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe'}, cb); + if (sub) return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe', sub:sub}, cb); async.waterfall([ function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) }, function(member, cb2){ diff --git a/views/options/settings.jade b/views/options/settings.jade index b1bee5e7ad..a32e8d6b0a 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -243,10 +243,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template') div(ng-include="'partials/options.settings.subscription.perks.html'") div(ng-if='!p.customerId || (p.customerId && p.dateTerminated)') + + .form-group + each block in env.Content.subscriptionBlocks + .radio + label + input(type="radio", name="subRadio", value="#{block.months}", ng-model='subscription') + | #{block.months} Month(s) Recurring: $#{block.price} + h3(ng-if='(p.customerId && p.dateTerminated)') Resubscribe - 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 + a.btn.btn-primary(ng-click='showStripe({subscription:subscription})') Card + a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{subscription}}') PayPal div(ng-if='p.customerId') .btn.btn-primary(ng-if='!p.dateTerminated && p.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card .btn.btn-sm.btn-danger(ng-if='!p.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub')