mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-05-24 06:35:37 +00:00
feat(discount) google discount & use sub.key instead of sub.months
This commit is contained in:
parent
303d30a270
commit
edf66cbb59
15 changed files with 114 additions and 68 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -179,3 +179,5 @@ a.label
|
|||
|
||||
.white, .white a
|
||||
color: #fff !important
|
||||
.line-through
|
||||
text-decoration line-through
|
||||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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)')
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue