feat(discount) google discount & use sub.key instead of sub.months

This commit is contained in:
Tyler Renelle 2014-12-20 20:47:16 -07:00
parent 303d30a270
commit edf66cbb59
15 changed files with 114 additions and 68 deletions

View file

@ -34,10 +34,11 @@
},
"PAYPAL":{
"billing_plans": {
"1":"1",
"3":"3",
"6":"6",
"12":"12"
"basic_earned":"basic_earned",
"basic_3mo":"basic_3mo",
"basic_6mo":"basic_6mo",
"google_6mo":"google_6mo",
"basic_12mo":"basic_12mo"
},
"mode":"sandbox",
"client_id":"client_id",

View file

@ -179,3 +179,5 @@ a.label
.white, .white a
color: #fff !important
.line-through
text-decoration line-through

View file

@ -129,7 +129,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.gift = {
type: 'gems',
gems: {amount:0, fromBalance:true},
subscription: {months:0},
subscription: {key:''},
message:''
};
$scope.sendGift = function(uuid, gift){

View file

@ -181,5 +181,15 @@ habitrpg.controller('SettingsCtrl',
$scope.deleteWebhook = function(id) {
User.user.ops.deleteWebhook({params:{id:id}});
}
$scope.applyCoupon = function(coupon){
$http.get(ApiUrlService.get() + '/api/v2/coupons/valid-discount/'+coupon)
.success(function(){
Notification.text("Coupon applied!");
var subs = $scope.Content.subscriptionBlocks;
subs["basic_6mo"].discount = true;
subs["google_6mo"].discount = false;
});
}
}
]);

View file

@ -3,23 +3,12 @@
angular.module('paymentServices',[]).factory('Payments',
['$rootScope', 'User', '$http', 'Content',
function($rootScope, User, $http, Content) {
var user = User.user;
var plan = User.user.purchased.plan;
var Payments = {};
Payments.currentSub = _.find(Content.subscriptionBlocks, function(b){
switch (plan.paymentMethod) {
case 'Stripe':
case 'Paypal': // FIXME store paypalKey somewhere?
return b.key == plan.planId;
default: return undefined;
}
})
Payments.showStripe = function(data) {
var sub =
data.subscription ? data.subscription
: data.gift && data.gift.type=='subscription' ? data.gift.subscription.months
: data.gift && data.gift.type=='subscription' ? data.gift.subscription.key
: false;
sub = sub && Content.subscriptionBlocks[sub];
var amount = // 500 = $5
@ -36,7 +25,8 @@ function($rootScope, User, $http, Content) {
token: function(res) {
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
if (data.subscription) url += '&sub='+sub.months;
if (data.subscription) url += '&sub='+sub.key;
if (data.coupon) url += '&coupon='+data.coupon;
$http.post(url, res).success(function() {
window.location.reload(true);
}).error(function(res) {
@ -66,7 +56,7 @@ function($rootScope, User, $http, Content) {
Payments.cancelSubscription = function(){
if (!confirm(window.env.t('sureCancelSub'))) return;
window.location.href = '/' + plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + user._id + '&apiToken=' + user.apiToken;
window.location.href = '/' + User.user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.user.apiToken;
}
Payments.encodeGift = function(uuid, gift){

View file

@ -35,7 +35,7 @@ api.sendMessage = function(user, member, data){
msg = data.message
} else {
msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you ";
msg += (data.type=='gems') ? data.gems.amount + " gems!`" : data.subscription.months + " months of subscription!`";
msg += (data.type=='gems') ? data.gems.amount + " gems!`" : shared.content.subscriptionBlocks[data.subscription.key].months + " months of subscription!`";
msg += data.message;
}
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));

View file

@ -10,6 +10,8 @@ var paypal = require('./paypal');
var members = require('../members')
var async = require('async');
var iap = require('./iap');
var mongoose= require('mongoose');
var cc = require('coupon-code');
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
@ -29,8 +31,8 @@ exports.createSubscription = function(data, cb) {
var recipient = data.gift ? data.gift.member : data.user;
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case
var p = recipient.purchased.plan;
var months = +(data.gift ? data.gift.subscription.months : data.sub.months);
var block = shared.content.subscriptionBlocks[months];
var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
var months = +block.months;
if (data.gift) {
if (p.customerId && !p.dateTerminated) { // User has active plan
@ -114,6 +116,14 @@ exports.buyGems = function(data, cb) {
], cb);
}
exports.validCoupon = function(req, res, next){
mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
if (err) return next(err);
if (!coupon) return res.json(401, {err:"Invalid coupon code"});
return res.send(200);
});
}
exports.stripeCheckout = stripe.checkout;
exports.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit;
@ -126,4 +136,4 @@ exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn;
exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify;
exports.iapIosVerify = iap.iosVerify;

View file

@ -9,12 +9,14 @@ var logger = require('../../logging');
var ipn = require('paypal-ipn');
var paypal = require('paypal-rest-sdk');
var shared = require('habitrpg-shared');
var mongoose = require('mongoose');
var cc = require('coupon-code');
// 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);
block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
});
paypal.configure({
@ -30,23 +32,33 @@ var parseErr = function(res, err){
}
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
var sub = shared.content.subscriptionBlocks[req.query.sub];
async.waterfall([
function(cb){
if (!sub.discount) return cb(null, null);
if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
},
"payer": {
"payment_method": "paypal"
function(coupon, cb){
if (sub.discount && !coupon) return cb('Invalid coupon code.');
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
var billingAgreementAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"start_date": moment().add({minutes:5}).format(),
"plan": {
"id": sub.paypalKey
},
"payer": {
"payment_method": "paypal"
}
};
paypal.billingAgreement.create(billingAgreementAttributes, cb);
}
};
paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) {
], function(err, billingAgreement){
if (err) return parseErr(res, err);
// For approving subscription via Paypal, first redirect user to: approval_url
req.session.paypalBlock = req.query.sub;
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
res.redirect(approval_url);
});
@ -82,10 +94,10 @@ exports.createPayment = function(req, res) {
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);
: Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
var description = !gift ? "HabitRPG Gems"
: gift.type=='gems' ? "HabitRPG Gems (Gift)"
: gift.subscription.months + "mo. HabitRPG Subscription (Gift)";
: shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
var create_payment = {
"intent": "sale",
"payer": {

View file

@ -11,7 +11,7 @@ var paypal = require('paypal-rest-sdk');
var blocks = require('habitrpg-shared').content.subscriptionBlocks;
var live = nconf.get('PAYPAL:mode')=='live';
var OP = 'get'; // list create update remove
var OP = 'create'; // list create update remove
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
@ -72,7 +72,7 @@ switch(OP) {
});
break;
case "create":
paypal.billingPlan.create(blocks["12"].definition, function(err,plan){
paypal.billingPlan.create(blocks["google_6mo"].definition, function(err,plan){
if (err) return console.log(err);
if (plan.state == "ACTIVE")
return console.log({err:err, plan:plan});

View file

@ -4,6 +4,8 @@ var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
var shared = require('habitrpg-shared');
var mongoose = require('mongoose');
var cc = require('coupon-code');
/*
Setup Stripe response when posting payment
@ -17,16 +19,27 @@ exports.checkout = function(req, res, next) {
async.waterfall([
function(cb){
if (sub) {
stripe.customers.create({
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key
}, cb);
async.waterfall([
function(cb2){
if (!sub.discount) return cb2(null, null);
if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
},
function(coupon, cb2){
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
var customer = {
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key
};
stripe.customers.create(customer, cb2);
}
], cb);
} else {
stripe.charges.create({
amount: !gift ? "500" //"500" = $5
: gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.months].price*100
: gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ""+gift.gems.amount/4*100,
currency: "usd",
card: token

View file

@ -7,7 +7,7 @@ var autoinc = require('mongoose-id-autoinc');
var CouponSchema = new mongoose.Schema({
_id: {type: String, 'default': cc.generate},
event: {type:String, enum:['wondercon']},
event: {type:String, enum:['wondercon','google_6mo']},
user: {type: 'String', ref: 'User'}
});

View file

@ -20,4 +20,6 @@ router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, p
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post("/iap/ios/verify", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.iapIosVerify);
router.get("/api/v2/coupons/valid-discount/:code", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
module.exports = router;

View file

@ -8,7 +8,7 @@ var logging = require('./logging');
var isProd = nconf.get('NODE_ENV') === 'production';
var isDev = nconf.get('NODE_ENV') === 'development';
if (cluster.isMaster && (isDev || isProd)) {
if (false && cluster.isMaster && (isDev || isProd)) {
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
var cpus = require('os').cpus(),
cores = +nconf.get("CORES");

View file

@ -204,7 +204,7 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')} 
span.badge.badge-success(ng-show='_subscription.months>1') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(_subscription.months/3*5), 50] | min }}
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
tr
td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@ -214,8 +214,8 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')} 
div(ng-show='_subscription.months>1')
.badge.badge-success +{{Math.floor(_subscription.months/3)}} Mystic Hourglass
div(ng-show='_subscription.key!="basic_earned"')
.badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
tr
td
@ -228,7 +228,7 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template')
script(id='partials/options.settings.subscription.html',type='text/ng-template')
//-h2=env.t('individualSub')
.container-fluid(ng-init='_subscription={months:1}')
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
.row
.col-md-6
h3 Benefits
@ -241,7 +241,7 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='Payments.currentSub') Recurring ${{Payments.currentSub.price}} each {{Payments.currentSub.months}} Month(s) ({{user.purchased.plan.paymentMethod}})
p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
@ -254,16 +254,23 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group
each block in env.Content.subscriptionBlocks
.radio
label
input(type="radio", name="subRadio", value="#{block.months}", ng-model='_subscription.months')
//-| #{block.months} Month(s) Recurring: $#{block.price} #{env.t('monthUSD')}
| Recurring $#{block.price} each #{block.months} Month(s)
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
span(ng-show='block.original')
| Recurring <span class='line-through'>${{block.original}}</span> <span class='label label-success'>${{block.price}}</span> each {{block.months}} Month(s)
span(ng-hide='block.original')
| Recurring ${{block.price}} each {{block.months}} Month(s)
.form-inline
.form-group
input.form-control(type='text', ng-model='_subscription.coupon', placeholder='Promo Code')
.form-group
button.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)') Apply
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') Resubscribe
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.months})', ng-disabled='!_subscription.months') Card
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.months}}', ng-disabled='!_subscription.months') PayPal
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key') Card
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')

View file

@ -75,11 +75,10 @@ script(type='text/ng-template', id='modals/send-gift.html')
.panel-heading Subscription
.panel-body
.form-group
each block in env.Content.subscriptionBlocks
.radio
label
input(type="radio", name="subRadio", value="#{block.months}", ng-model='gift.subscription.months')
| #{block.months} Month(s): $#{block.price}
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='gift.subscription.key')
| {{block.months}} Month(s): ${{block.price}}
textarea.form-control(rows='3', ng-model='gift.message', placeholder='Personal message (optional)')