v3: port coupons

This commit is contained in:
Matteo Pagliazzi
2016-04-02 16:37:55 +02:00
parent 731ac86244
commit de74fae0b4
12 changed files with 475 additions and 73 deletions
+125
View File
@@ -0,0 +1,125 @@
import csvStringify from '../../libs/api-v3/csvStringify';
import {
authWithHeaders,
authWithSession,
} from '../../middlewares/api-v3/auth';
import cron from '../../middlewares/api-v3/cron';
import { ensureSudo } from '../../middlewares/api-v3/ensureAccessRight';
import { model as Coupon } from '../../models/coupon';
import _ from 'lodash';
import couponCode from 'coupon-code';
let api = {};
/**
* @api {get} /coupons Get coupons (sudo users only)
* @apiVersion 3.0.0
* @apiName GetCoupons
* @apiGroup Coupon
*
* @apiSuccess string Coupons in CSV format
*/
api.getCoupons = {
method: 'GET',
url: '/coupons',
middlewares: [authWithSession, cron, ensureSudo],
async handler (req, res) {
let coupons = await Coupon.find().sort('createdAt').lean().exec();
let output = [['code', 'event', 'date', 'user']].concat(_.map(coupons, coupon => {
return [coupon._id, coupon.event, coupon.createdAt, coupon.user];
}));
let csv = await csvStringify(output);
res.set({
'Content-Type': 'text/csv',
'Content-disposition': `attachment; filename=habitica-coupons.csv`,
});
res.status(200).send(csv);
},
};
/**
* @api {post} /coupons/generate/:event Generate coupons for an event (sudo users only)
* @apiVersion 3.0.0
* @apiName GenerateCoupons
* @apiGroup Coupon
*
* @apiParam {string} event The event for which the coupon should be generated
* @apiParam {number} count Query parameter to specify the number of coupon codes to generate
*
* @apiSuccess array Generated coupons
*/
api.generateCoupons = {
method: 'POST',
url: '/coupons/generate/:event',
middlewares: [authWithHeaders(), cron, ensureSudo],
async handler (req, res) {
req.checkParams('event', res.t('eventRequired')).notEmpty();
req.checkQuery('count', res.t('countRequired')).notEmpty().isNumeric();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let coupons = await Coupon.generate(req.params.event, req.query.count);
res.respond(200, coupons);
},
};
/**
* @api {post} /user/coupon/:code Enter coupon code
* @apiVersion 3.0.0
* @apiName EnterCouponCode
* @apiGroup Coupon
*
* @apiParam {string} code The coupon code to apply
*
* @apiSuccess object User object
*/
api.enterCouponCode = {
method: 'POST',
url: '/coupons/enter/:code',
middlewares: [authWithHeaders(), cron],
async handler (req, res) {
let user = res.locals.user;
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
await Coupon.apply(user, req, req.params.code);
res.respond(200, user);
},
};
/**
* @api {post} /coupons/validate/:code Validate a coupon code
* @apiVersion 3.0.0
* @apiName ValidateCoupon
* @apiGroup Coupon
*
* @apiSuccess valid {boolean} true or false
*/
api.validateCoupon = {
method: 'POST',
url: '/coupons/validate/:code',
middlewares: [authWithHeaders(true)],
async handler (req, res) {
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let valid = false;
let code = couponCode.validate(req.params.code);
if (code) {
let coupon = await Coupon.findOne({_id: code}).exec();
valid = coupon ? true : false;
}
res.respond(200, {valid});
},
};
module.exports = api;
+3 -13
View File
@@ -1,9 +1,9 @@
import { authWithHeaders } from '../../middlewares/api-v3/auth';
import { ensureAdmin } from '../../middlewares/api-v3/ensureAccessRight';
import cron from '../../middlewares/api-v3/cron';
import { model as User } from '../../models/user';
import {
NotFound,
NotAuthorized,
} from '../../libs/api-v3/errors';
import _ from 'lodash';
@@ -90,9 +90,8 @@ const heroAdminFields = 'contributor balance profile.name purchased items auth';
api.getHero = {
method: 'GET',
url: '/hall/heroes/:heroId',
middlewares: [authWithHeaders(), cron],
middlewares: [authWithHeaders(), cron, ensureAdmin],
async handler (req, res) {
let user = res.locals.user;
let heroId = req.params.heroId;
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty().isUUID();
@@ -100,10 +99,6 @@ api.getHero = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
if (!user.contributor.admin) {
throw new NotAuthorized(res.t('noAdminAccess'));
}
let hero = await User
.findById(heroId)
.select(heroAdminFields)
@@ -132,9 +127,8 @@ const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0};
api.updateHero = {
method: 'PUT',
url: '/hall/heroes/:heroId',
middlewares: [authWithHeaders(), cron],
middlewares: [authWithHeaders(), cron, ensureAdmin],
async handler (req, res) {
let user = res.locals.user;
let heroId = req.params.heroId;
let updateData = req.body;
@@ -143,10 +137,6 @@ api.updateHero = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
if (!user.contributor.admin) {
throw new NotAuthorized(res.t('noAdminAccess'));
}
let hero = await User.findById(heroId).exec();
if (!hero) throw new NotFound(res.t('userWithIDNotFound', {userId: heroId}));
@@ -0,0 +1,23 @@
import {
NotAuthorized,
} from '../../libs/api-v3/errors';
export function ensureAdmin (req, res, next) {
let user = res.locals.user;
if (!user.contributor.admin) {
return next(new NotAuthorized(res.t('noAdminAccess')));
}
next();
}
export function ensureSudo (req, res, next) {
let user = res.locals.user;
if (!user.contributor.sudo) {
return next(new NotAuthorized(res.t('noSudoAccess')));
}
next();
}
+52 -53
View File
@@ -1,59 +1,58 @@
var mongoose = require("mongoose");
var shared = require('../../../common');
var _ = require('lodash');
var async = require('async');
var cc = require('coupon-code');
var autoinc = require('mongoose-id-autoinc');
/* eslint-disable camelcase */
var CouponSchema = new mongoose.Schema({
_id: {type: String, 'default': cc.generate},
event: {type:String, enum:['wondercon','google_6mo']},
user: {type: 'String', ref: 'User'}
import mongoose from 'mongoose';
import _ from 'lodash';
import shared from '../../../common';
import couponCode from 'coupon-code';
import baseModel from '../libs/api-v3/baseModel';
import {
BadRequest,
NotAuthorized,
} from '../libs/api-v3/errors';
export let schema = new mongoose.Schema({
event: {type: String, enum: ['wondercon', 'google_6mo']},
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){
async.auto({
get_coupon: function (cb) {
mongoose.model('Coupon').findById(cc.validate(code), cb);
},
apply_coupon: ['get_coupon', function (cb, results) {
if (!results.get_coupon) return cb("Invalid coupon code");
if (results.get_coupon.user) return cb("Coupon already used");
switch (results.get_coupon.event) {
case 'wondercon':
user.items.gear.owned.eyewear_special_wondercon_red = true;
user.items.gear.owned.eyewear_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.extra = {signupEvent: 'wondercon'};
user.save(cb);
break;
}
}],
expire_coupon: ['apply_coupon', function (cb, results) {
results.get_coupon.user = user._id;
results.get_coupon.save(cb);
}]
}, function(err, results){
if (err) return next(err);
next(null,results.apply_coupon[0]);
})
}
CouponSchema.plugin(autoinc.plugin, {
model: 'Coupon',
field: 'seq'
schema.plugin(baseModel, {
timestamps: true,
});
module.exports.schema = CouponSchema;
module.exports.model = mongoose.model("Coupon", CouponSchema);
// Add _id field after plugin to override default _id format
schema.add({
_id: {type: String, default: couponCode.generate},
});
schema.statics.generate = async function generateCoupons (event, count = 1) {
let coupons = _.times(count, () => {
return {event};
});
return await this.create(coupons);
};
schema.statics.apply = async function applyCoupon (user, req, code) {
let coupon = await this.findById(couponCode.validate(code)).exec();
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon', req.language));
if (coupon.user) throw new NotAuthorized(shared.i18n.t('couponUsed', req.language));
if (coupon.event === 'wondercon') {
user.items.gear.owned.eyewear_special_wondercon_red = true;
user.items.gear.owned.eyewear_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.extra = {signupEvent: 'wondercon'};
}
await user.save();
coupon.user = user._id;
await coupon.save();
};
module.exports.schema = schema;
export let model = mongoose.model('Coupon', schema);