feat(coupons): add coupon-code feature

This commit is contained in:
Tyler Renelle 2014-04-16 11:33:03 -06:00
parent e2eff3b185
commit a608419bba
10 changed files with 192 additions and 4 deletions

View file

@ -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",

View file

@ -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"

View file

@ -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
View 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
View 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);

View file

@ -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
View 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;

View file

@ -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);

View file

@ -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

View file

@ -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