mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-15 12:07:43 +00:00
feat(coupons): add coupon-code feature
This commit is contained in:
parent
e2eff3b185
commit
a608419bba
10 changed files with 192 additions and 4 deletions
|
|
@ -51,7 +51,9 @@
|
|||
"universal-analytics": "~0.3.2",
|
||||
"paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg",
|
||||
"paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca",
|
||||
"paypal-ipn": "~1.0.1"
|
||||
"paypal-ipn": "~1.0.1",
|
||||
"coupon-code": "~0.3.0",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4"
|
||||
},
|
||||
"private": true,
|
||||
"subdomain": "habitrpg",
|
||||
|
|
|
|||
|
|
@ -191,6 +191,10 @@ window.habitrpg = angular.module('habitrpg',
|
|||
url: "/export",
|
||||
templateUrl: "partials/options.settings.export.html"
|
||||
})
|
||||
.state('options.settings.coupon', {
|
||||
url: "/coupon",
|
||||
templateUrl: "partials/options.settings.coupon.html"
|
||||
})
|
||||
.state('options.settings.subscription', {
|
||||
url: "/subscription",
|
||||
templateUrl: "partials/options.settings.subscription.html"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
// Make user and settings available for everyone through root scope.
|
||||
habitrpg.controller('SettingsCtrl',
|
||||
['$scope', 'User', '$rootScope', '$http', 'API_URL', 'Guide', '$location', '$timeout',
|
||||
function($scope, User, $rootScope, $http, API_URL, Guide, $location, $timeout) {
|
||||
['$scope', 'User', '$rootScope', '$http', 'API_URL', 'Guide', '$location', '$timeout', 'Notification',
|
||||
function($scope, User, $rootScope, $http, API_URL, Guide, $location, $timeout, Notification) {
|
||||
|
||||
// FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest
|
||||
// $scope.auth = function (id, token) {
|
||||
|
|
@ -112,5 +112,21 @@ habitrpg.controller('SettingsCtrl',
|
|||
window.location.href = '/logout';
|
||||
});
|
||||
}
|
||||
|
||||
$scope.enterCoupon = function(code) {
|
||||
$http.post(API_URL + '/api/v2/user/coupon/' + code).success(function(res,code){
|
||||
if (code!==200) return;
|
||||
User.sync();
|
||||
Notification.text('Coupon applied! Check your inventory');
|
||||
});
|
||||
}
|
||||
$scope.generateCodes = function(codes){
|
||||
$http.post(API_URL + '/api/v2/coupons/generate/'+codes.event+'?count='+(codes.count || 1))
|
||||
.success(function(res,code){
|
||||
$scope._codes = {};
|
||||
if (code!==200) return;
|
||||
window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+'&apiToken='+User.user.apiToken;
|
||||
})
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
52
src/controllers/coupon.js
Normal file
52
src/controllers/coupon.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
var _ = require('lodash');
|
||||
var Coupon = require('./../models/coupon').model;
|
||||
var api = module.exports;
|
||||
var csv = require('express-csv');
|
||||
var async = require('async');
|
||||
|
||||
api.ensureAdmin = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (!user.contributor.sudo) return res.json(401, {err:"You don't have admin access"});
|
||||
next();
|
||||
}
|
||||
|
||||
api.generateCoupons = function(req,res,next) {
|
||||
Coupon.generate(req.params.event, req.query.count, function(err){
|
||||
if(err) return next(err);
|
||||
res.send(200);
|
||||
});
|
||||
|
||||
// var skip, count=req.params.count || 1;
|
||||
// async.waterfall([
|
||||
// function(cb) {
|
||||
// Coupon.findOne({}, {}, {sort: '-seq'}, cb);
|
||||
// },
|
||||
// function(_lastCoupon,cb){
|
||||
// skip = _lastCoupon ? _lastCoupon.seq : 0;
|
||||
// Coupon.generate(req.params.event, count, cb);
|
||||
// }
|
||||
// ], function(err){
|
||||
// if (err) return next(err);
|
||||
// //res.redirect('/api/v2/coupons?skip='+skip+'&limit='+count);
|
||||
// res.send(200);
|
||||
// })
|
||||
}
|
||||
|
||||
api.getCoupons = function(req,res,next) {
|
||||
var options = {sort:'-seq'};
|
||||
if (req.query.limit) options.limit = req.query.limit;
|
||||
if (req.query.skip) options.limit = req.query.skip;
|
||||
Coupon.find({},{}, options, function(err,coupons){
|
||||
//res.header('Content-disposition', 'attachment; filename=coupons.csv');
|
||||
res.csv([['seq','event','code']].concat(_.map(coupons, function(c){
|
||||
return [c.seq, c.event, c._id];
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
api.enterCode = function(req,res,next) {
|
||||
Coupon.apply(res.locals.user,req.params.code,function(err,user){
|
||||
if (err) return res.json(400,{err:err});
|
||||
res.json(user);
|
||||
});
|
||||
}
|
||||
61
src/models/coupon.js
Normal file
61
src/models/coupon.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
var mongoose = require("mongoose");
|
||||
var shared = require('habitrpg-shared');
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
var cc = require('coupon-code');
|
||||
var autoinc = require('mongoose-id-autoinc');
|
||||
|
||||
var CouponSchema = new mongoose.Schema({
|
||||
_id: {type: String, 'default': cc.generate},
|
||||
event: {type:String, enum:['wondercon']},
|
||||
user: {type: 'String', ref: 'User'}
|
||||
});
|
||||
|
||||
CouponSchema.statics.generate = function(event, count, callback) {
|
||||
async.times(count, function(n,cb){
|
||||
mongoose.model('Coupon').create({event: event}, cb);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
CouponSchema.statics.apply = function(user, code, next){
|
||||
var _coupon,_user;
|
||||
async.waterfall([
|
||||
function(cb) {
|
||||
mongoose.model('Coupon').findById(code, cb);
|
||||
},
|
||||
function(coupon, cb) {
|
||||
_coupon = coupon;
|
||||
if (_.isEmpty(cc.validate(code)) || !coupon) return cb("Invalid coupon code");
|
||||
if (coupon.user) return cb("Coupon already used");
|
||||
switch (coupon.event) {
|
||||
case 'wondercon':
|
||||
user.items.gear.owned.headAccessory_special_wondercon_red = true;
|
||||
user.items.gear.owned.headAccessory_special_wondercon_black = true;
|
||||
user.items.gear.owned.back_special_wondercon_black = true;
|
||||
user.items.gear.owned.back_special_wondercon_red = true;
|
||||
user.items.gear.owned.body_special_wondercon_red = true;
|
||||
user.items.gear.owned.body_special_wondercon_black = true;
|
||||
user.items.gear.owned.body_special_wondercon_gold = true;
|
||||
user.save(cb);
|
||||
break;
|
||||
}
|
||||
},
|
||||
function(user, count, cb){
|
||||
_user = user;
|
||||
_coupon.user = user._id;
|
||||
_coupon.save(cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return next(err);
|
||||
next(null,_user);
|
||||
})
|
||||
}
|
||||
|
||||
CouponSchema.plugin(autoinc.plugin, {
|
||||
model: 'Coupon',
|
||||
field: 'seq'
|
||||
});
|
||||
|
||||
module.exports.schema = CouponSchema;
|
||||
module.exports.model = mongoose.model("Coupon", CouponSchema);
|
||||
|
||||
|
|
@ -68,6 +68,7 @@ var UserSchema = new Schema({
|
|||
contributor: {
|
||||
level: Number, // 1-7, see https://trello.com/c/wkFzONhE/277-contributor-gear
|
||||
admin: Boolean,
|
||||
sudo: Boolean,
|
||||
text: String, // Artisan, Friend, Blacksmith, etc
|
||||
contributions: String, // a markdown textarea to list their contributions + links
|
||||
critical: String
|
||||
|
|
|
|||
11
src/routes/coupon.js
Normal file
11
src/routes/coupon.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
var nconf = require('nconf');
|
||||
var express = require('express');
|
||||
var router = new express.Router();
|
||||
var auth = require('../controllers/auth');
|
||||
var coupon = require('../controllers/coupon');
|
||||
|
||||
router.get('/api/v2/coupons', auth.authWithUrl, coupon.ensureAdmin, coupon.getCoupons);
|
||||
router.post('/api/v2/coupons/generate/:event', auth.auth, coupon.ensureAdmin, coupon.generateCoupons);
|
||||
router.post('/api/v2/user/coupon/:code', auth.auth, coupon.enterCode);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -25,6 +25,7 @@ if (cluster.isMaster && (isDev || isProd)) {
|
|||
var http = require("http");
|
||||
var path = require("path");
|
||||
var swagger = require("swagger-node-express");
|
||||
var autoinc = require('mongoose-id-autoinc');
|
||||
|
||||
var middleware = require('./middleware');
|
||||
|
||||
|
|
@ -38,10 +39,12 @@ if (cluster.isMaster && (isDev || isProd)) {
|
|||
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }
|
||||
};
|
||||
mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, function(err) {
|
||||
var db = mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, function(err) {
|
||||
if (err) throw err;
|
||||
logging.info('Connected with Mongoose');
|
||||
});
|
||||
autoinc.init(db);
|
||||
|
||||
// load schemas & models
|
||||
require('./models/challenge');
|
||||
require('./models/group');
|
||||
|
|
@ -125,6 +128,7 @@ if (cluster.isMaster && (isDev || isProd)) {
|
|||
app.use(require('./routes/pages').middleware);
|
||||
app.use(require('./routes/payments').middleware);
|
||||
app.use(require('./routes/auth').middleware);
|
||||
app.use(require('./routes/coupon').middleware);
|
||||
var v2 = express();
|
||||
app.use('/api/v2', v2);
|
||||
app.use('/api/v1', require('./routes/apiv1').middleware);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ script(id='partials/options.settings.html', type="text/ng-template")
|
|||
li(ng-class="{ active: $state.includes('options.settings.export') }")
|
||||
a(ui-sref='options.settings.export')
|
||||
=env.t('dataExport')
|
||||
li(ng-class="{ active: $state.includes('options.settings.coupon') }")
|
||||
a(ui-sref='options.settings.coupon')
|
||||
| Coupon
|
||||
li(ng-class="{ active: $state.includes('options.settings.subscription') }")
|
||||
a(ui-sref='options.settings.subscription')=env.t('subscription')
|
||||
|
||||
|
|
@ -68,6 +71,14 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
|||
|
|
||||
=env.t('subWarning3')
|
||||
|
||||
//-.panel.panel-default
|
||||
.panel-heading
|
||||
span.hint(popover='We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)', popover-trigger='mouseenter', popover-placement='right') Coupon
|
||||
.panel-body
|
||||
form.form-inline(role='form',ng-submit='enterCoupon(_couponCode)')
|
||||
input.form-control(type='text', ng-model='_couponCode', placeholder='Enter Coupon Code')
|
||||
button.btn.btn-primary(type='submit') Submit
|
||||
|
||||
//- Why is ng-if='user.auth.local' validating for users *without* user.auth.local (facebook users)? adding .username here for extra
|
||||
div(ng-if='user.auth.local.username')
|
||||
.panel.panel-default
|
||||
|
|
@ -91,6 +102,30 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
|||
a.btn.btn-danger(ng-click='openModal("reset", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount')
|
||||
a.btn.btn-danger(ng-click='openModal("delete", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount')
|
||||
|
||||
script(type='text/ng-template', id='partials/options.settings.coupon.html')
|
||||
.containter-fluid
|
||||
.row
|
||||
.col-md-6
|
||||
h2 Coupon
|
||||
form.form-inline(role='form',ng-submit='enterCoupon(_couponCode)')
|
||||
input.form-control(type='text', ng-model='_couponCode', placeholder='Enter Coupon Code')
|
||||
button.btn.btn-primary(type='submit') Submit
|
||||
div
|
||||
small We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)
|
||||
div(ng-if='user.contributor.sudo')
|
||||
hr
|
||||
h4 Generate Codes
|
||||
form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}')
|
||||
.form-group
|
||||
input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')")
|
||||
.form-group
|
||||
input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)")
|
||||
.form-group
|
||||
button.btn.btn-primary(type='submit') Generate
|
||||
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes
|
||||
|
||||
|
||||
|
||||
script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
.containter-fluid
|
||||
.row
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ nav.toolbar(ng-controller='AuthCtrl')
|
|||
a(ui-sref='options.settings.api')=env.t('API')
|
||||
li
|
||||
a(ui-sref='options.settings.export')=env.t('export')
|
||||
li
|
||||
a(ui-sref='options.settings.coupon') Coupon
|
||||
li
|
||||
a(ui-sref='options.settings.subscription')=env.t('subscription')
|
||||
li
|
||||
|
|
|
|||
Loading…
Reference in a new issue