diff --git a/package.json b/package.json index f462b7088f..a9329aab6a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/js/app.js b/public/js/app.js index faee34ecfa..289c715b07 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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" diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index 617273253a..5bbb13be86 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -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; + }) + } } ]); diff --git a/src/controllers/coupon.js b/src/controllers/coupon.js new file mode 100644 index 0000000000..fb1a5c251d --- /dev/null +++ b/src/controllers/coupon.js @@ -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); + }); +} \ No newline at end of file diff --git a/src/models/coupon.js b/src/models/coupon.js new file mode 100644 index 0000000000..8905ed0320 --- /dev/null +++ b/src/models/coupon.js @@ -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); + diff --git a/src/models/user.js b/src/models/user.js index b52c8e3df5..6d1f088e3d 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -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 diff --git a/src/routes/coupon.js b/src/routes/coupon.js new file mode 100644 index 0000000000..fff2e2ea82 --- /dev/null +++ b/src/routes/coupon.js @@ -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; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 1a48dbf5b2..ac0a7dfa80 100644 --- a/src/server.js +++ b/src/server.js @@ -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); diff --git a/views/options/settings.jade b/views/options/settings.jade index db5ea9362e..d101fd92bd 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -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 diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 264335990b..a9a61d9611 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -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