diff --git a/config.json.example b/config.json.example
index e34d187838..6933f92426 100644
--- a/config.json.example
+++ b/config.json.example
@@ -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",
diff --git a/public/css/index.styl b/public/css/index.styl
index 381ab03082..998fcad48e 100644
--- a/public/css/index.styl
+++ b/public/css/index.styl
@@ -179,3 +179,5 @@ a.label
.white, .white a
color: #fff !important
+.line-through
+ text-decoration line-through
\ No newline at end of file
diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js
index 338338f76b..fe30af9239 100644
--- a/public/js/controllers/groupsCtrl.js
+++ b/public/js/controllers/groupsCtrl.js
@@ -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){
diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js
index 4481ed2480..a7927d5d0c 100644
--- a/public/js/controllers/settingsCtrl.js
+++ b/public/js/controllers/settingsCtrl.js
@@ -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;
+ });
+ }
}
]);
diff --git a/public/js/services/paymentServices.js b/public/js/services/paymentServices.js
index 0fd7682512..3a0f9b21da 100644
--- a/public/js/services/paymentServices.js
+++ b/public/js/services/paymentServices.js
@@ -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){
diff --git a/src/controllers/members.js b/src/controllers/members.js
index 26092e872c..25ffdce25d 100644
--- a/src/controllers/members.js
+++ b/src/controllers/members.js
@@ -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));
diff --git a/src/controllers/payments/index.js b/src/controllers/payments/index.js
index b0c59f643c..fea3c73025 100644
--- a/src/controllers/payments/index.js
+++ b/src/controllers/payments/index.js
@@ -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;
\ No newline at end of file
diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js
index 9c7a1d4f3a..f69ab306da 100644
--- a/src/controllers/payments/paypal.js
+++ b/src/controllers/payments/paypal.js
@@ -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": {
diff --git a/src/controllers/payments/paypalBillingSetup.js b/src/controllers/payments/paypalBillingSetup.js
index 38b70c0101..14721e22d1 100644
--- a/src/controllers/payments/paypalBillingSetup.js
+++ b/src/controllers/payments/paypalBillingSetup.js
@@ -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});
diff --git a/src/controllers/payments/stripe.js b/src/controllers/payments/stripe.js
index 117fc29175..74172cc8ab 100644
--- a/src/controllers/payments/stripe.js
+++ b/src/controllers/payments/stripe.js
@@ -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
diff --git a/src/models/coupon.js b/src/models/coupon.js
index 497b45697f..4f2680546c 100644
--- a/src/models/coupon.js
+++ b/src/models/coupon.js
@@ -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'}
});
diff --git a/src/routes/payments.js b/src/routes/payments.js
index adc9a65629..118f923689 100644
--- a/src/routes/payments.js
+++ b/src/routes/payments.js
@@ -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;
\ No newline at end of file
diff --git a/src/server.js b/src/server.js
index 8e551f7891..7c53317e23 100644
--- a/src/server.js
+++ b/src/server.js
@@ -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");
diff --git a/views/options/settings.jade b/views/options/settings.jade
index e8e50b2405..3ba5192ec8 100644
--- a/views/options/settings.jade
+++ b/views/options/settings.jade
@@ -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')} {{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
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 ${{block.original}} ${{block.price}} 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')
\ No newline at end of file
diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade
index 7f44481a17..fbe64c6279 100644
--- a/views/shared/modals/members.jade
+++ b/views/shared/modals/members.jade
@@ -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)')