diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 093a4222..ea809a07 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +- Added travel model +- Added route to create travel +- Added routes to look up travel by id, email or self +- Added routes to status or offer of an existing travel + ## [2.2.0](https://github.com/hackmcgill/hackerapi/tree/2.2.0) - 2020-01-12 ### Added diff --git a/app.js b/app.js index 4de92284..2c6359a5 100755 --- a/app.js +++ b/app.js @@ -25,6 +25,7 @@ const accountRouter = require("./routes/api/account"); const authRouter = require("./routes/api/auth"); const hackerRouter = require("./routes/api/hacker"); const teamRouter = require("./routes/api/team"); +const travelRouter = require("./routes/api/travel"); const sponsorRouter = require("./routes/api/sponsor"); const searchRouter = require("./routes/api/search"); const settingsRouter = require("./routes/api/settings"); @@ -87,6 +88,8 @@ hackerRouter.activate(apiRouter); Services.log.info("Hacker router activated"); teamRouter.activate(apiRouter); Services.log.info("Team router activated"); +travelRouter.activate(apiRouter); +Services.log.info("Travel router activated") sponsorRouter.activate(apiRouter); Services.log.info("Sponsor router activated"); volunteerRouter.activate(apiRouter); diff --git a/constants/error.constant.js b/constants/error.constant.js index f2e6c535..60ac33ca 100644 --- a/constants/error.constant.js +++ b/constants/error.constant.js @@ -7,6 +7,7 @@ const RESUME_404_MESSAGE = "Resume not found"; const SPONSOR_404_MESSAGE = "Sponsor not found"; const VOLUNTEER_404_MESSAGE = "Volunteer not found"; const SETTINGS_404_MESSAGE = "Settings not found"; +const TRAVEL_404_MESSAGE = "Travel not found"; const ACCOUNT_TYPE_409_MESSAGE = "Wrong account type"; const ACCOUNT_EMAIL_409_MESSAGE = "Email already in use"; @@ -44,6 +45,7 @@ const EMAIL_500_MESSAGE = "Error while generating email"; const GENERIC_500_MESSAGE = "Internal error"; const LOGIN_500_MESSAGE = "Error while logging in"; const ROLE_CREATE_500_MESSAGE = "Error while creating role"; +const TRAVEL_CREATE_500_MESSAGE = "Error while creating travel"; module.exports = { ACCOUNT_404_MESSAGE: ACCOUNT_404_MESSAGE, @@ -83,5 +85,7 @@ module.exports = { TEAM_READ_500_MESSAGE: TEAM_READ_500_MESSAGE, VOLUNTEER_404_MESSAGE: VOLUNTEER_404_MESSAGE, SPONSOR_UPDATE_500_MESSAGE: SPONSOR_UPDATE_500_MESSAGE, - SETTINGS_404_MESSAGE: SETTINGS_404_MESSAGE + SETTINGS_404_MESSAGE: SETTINGS_404_MESSAGE, + TRAVEL_404_MESSAGE: TRAVEL_404_MESSAGE, + TRAVEL_CREATE_500_MESSAGE: TRAVEL_CREATE_500_MESSAGE }; diff --git a/constants/general.constant.js b/constants/general.constant.js index 32cd591b..cb0c4c06 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -28,6 +28,23 @@ const HACKER_STATUSES = [ // This date is Jan 6, 2020 00:00:00 GMT -0500 const APPLICATION_CLOSE_TIME = 1578286800000; +const TRAVEL_STATUS_NONE = "None"; // Hacker has not been offered compensation for travelling +const TRAVEL_STATUS_BUS = "Bus"; // Hacker is taking bus to hackathon +const TRAVEL_STATUS_POLICY = "Policy"; // Hacker has been offer some reimbursement, but we are waiting for hacker to accept travel policy first +const TRAVEL_STATUS_OFFERED = "Offered"; // Hacker has been offered some amount of compensation for travelling, but we have not verified their reciepts yet +const TRAVEL_STATUS_VALID = "Valid"; // Hacker has been offered some amount of compensation for travelling and have uploaded reciepts which we have confirmed to be an approprate amount +const TRAVEL_STATUS_INVALID = "Invalid"; // Hacker has been offered some amount of compensation for travelling but have uploaded reciepts which we have confirmed to be an inapproprate amount +const TRAVEL_STATUS_CLAIMED = "Claimed"; // Hacker has been offered some amount of compensation and has recieved such the funds +const TRAVEL_STATUSES = [ + TRAVEL_STATUS_NONE, + TRAVEL_STATUS_BUS, + TRAVEL_STATUS_POLICY, + TRAVEL_STATUS_OFFERED, + TRAVEL_STATUS_VALID, + TRAVEL_STATUS_INVALID, + TRAVEL_STATUS_CLAIMED +]; + const SAMPLE_DIET_RESTRICTIONS = [ "None", "Vegan", @@ -160,6 +177,14 @@ module.exports = { HACKER_STATUS_WITHDRAWN: HACKER_STATUS_WITHDRAWN, HACKER_STATUS_CHECKED_IN: HACKER_STATUS_CHECKED_IN, HACKER_STATUSES: HACKER_STATUSES, + TRAVEL_STATUS_NONE: TRAVEL_STATUS_NONE, + TRAVEL_STATUS_BUS: TRAVEL_STATUS_BUS, + TRAVEL_STATUS_POLICY: TRAVEL_STATUS_POLICY, + TRAVEL_STATUS_OFFERED: TRAVEL_STATUS_OFFERED, + TRAVEL_STATUS_VALID: TRAVEL_STATUS_VALID, + TRAVEL_STATUS_INVALID: TRAVEL_STATUS_INVALID, + TRAVEL_STATUS_CLAIMED: TRAVEL_STATUS_CLAIMED, + TRAVEL_STATUSES: TRAVEL_STATUSES, APPLICATION_CLOSE_TIME: APPLICATION_CLOSE_TIME, REQUEST_TYPES: REQUEST_TYPES, JOB_INTERESTS: JOB_INTERESTS, diff --git a/constants/role.constant.js b/constants/role.constant.js index b2f00147..f9917247 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -38,6 +38,14 @@ const hackerRole = { Constants.Routes.hackerRoutes.patchSelfConfirmationById, Constants.Routes.hackerRoutes.getSelf, + Constants.Routes.travelRoutes.getSelf, + Constants.Routes.travelRoutes.getSelfById, + Constants.Routes.travelRoutes.getAnyById, + Constants.Routes.travelRoutes.getSelfByEmail, + Constants.Routes.travelRoutes.getAnyByEmail, + Constants.Routes.travelRoutes.patchAnyStatusById, + Constants.Routes.travelRoutes.patchAnyOfferById, + Constants.Routes.teamRoutes.join, Constants.Routes.teamRoutes.patchSelfById, Constants.Routes.teamRoutes.post, diff --git a/constants/routes.constant.js b/constants/routes.constant.js index 3e141cc4..e9c21ed0 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -150,6 +150,41 @@ const hackerRoutes = { } }; +const travelRoutes = { + getSelf: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/travel/self/" + }, + getSelfById: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/travel/" + Constants.ROLE_CATEGORIES.SELF + }, + getAnyById: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/travel/" + Constants.ROLE_CATEGORIES.ALL + }, + getSelfByEmail: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.SELF + }, + getAnyByEmail: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.ALL + }, + post: { + requestType: Constants.REQUEST_TYPES.POST, + uri: "/api/travel/" + }, + patchAnyStatusById: { + requestType: Constants.REQUEST_TYPES.PATCH, + uri: "/api/travel/status/" + Constants.ROLE_CATEGORIES.ALL + }, + patchAnyOfferById: { + requestType: Constants.REQUEST_TYPES.PATCH, + uri: "/api/travel/offer/" + Constants.ROLE_CATEGORIES.ALL + } +} + const sponsorRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, @@ -263,6 +298,7 @@ const allRoutes = { Auth: authRoutes, Account: accountRoutes, Hacker: hackerRoutes, + Travel: travelRoutes, Sponsor: sponsorRoutes, Team: teamRoutes, Volunteer: volunteerRoutes, @@ -302,6 +338,7 @@ module.exports = { authRoutes: authRoutes, accountRoutes: accountRoutes, hackerRoutes: hackerRoutes, + travelRoutes: travelRoutes, sponsorRoutes: sponsorRoutes, teamRoutes: teamRoutes, volunteerRoutes: volunteerRoutes, diff --git a/constants/success.constant.js b/constants/success.constant.js index 8fda8b91..e908f651 100644 --- a/constants/success.constant.js +++ b/constants/success.constant.js @@ -27,6 +27,10 @@ const HACKER_SENT_DAY_OF = "Hacker day-of email sent."; const RESUME_UPLOAD = "Resume upload successful."; const RESUME_DOWNLOAD = "Resume download successful."; +const TRAVEL_READ = "Travel retrieval successful."; +const TRAVEL_CREATE = "Travel creation successful."; +const TRAVEL_UPDATE = "Travel update successful."; + const ROLE_CREATE = "Role creation successful."; const SEARCH_QUERY = "Query search successful. Returning results."; @@ -78,6 +82,10 @@ module.exports = { RESUME_UPLOAD: RESUME_UPLOAD, RESUME_DOWNLOAD: RESUME_DOWNLOAD, + TRAVEL_READ: TRAVEL_READ, + TRAVEL_CREATE: TRAVEL_CREATE, + TRAVE_UPDATE: TRAVEL_UPDATE, + ROLE_CREATE: ROLE_CREATE, SEARCH_QUERY: SEARCH_QUERY, diff --git a/controllers/travel.controller.js b/controllers/travel.controller.js new file mode 100644 index 00000000..d69a50a7 --- /dev/null +++ b/controllers/travel.controller.js @@ -0,0 +1,66 @@ +"use strict"; +const Constants = { + Success: require("../constants/success.constant"), + Error: require("../constants/error.constant") +}; + +function okay(req, res) { + return res.status(200).json({ + message: "good" + }); +} + +/** + * @function showTravel + * @param {{body: {travel: Object}}} req + * @param {*} res + * @return {JSON} Success status and travel object + * @description Returns the JSON of travel object located in req.body.travel + */ +function showTravel(req, res) { + return res.status(200).json({ + message: Constants.Success.TRAVEL_READ, + data: req.body.travel.toJSON() + }); +} + +/** + * @function createTravel + * @param {{body: {travel: {_id: ObjectId, accountId: ObjectId, hackerId: objectId, status: string, request: number, offer: number}}}} req + * @param {*} res + * @return {JSON} Success status + * @description + * Create a travel's record based off information stored in req.body.travel + * Returns a 200 status for the created travel. + */ +function createdTravel(req, res) { + return res.status(200).json({ + message: Constants.Success.TRAVEL_CREATE, + data: req.body.travel.toJSON() + }); +} + +/** + * @function updatedTravel + * @param {{params: {id: ObjectId}, body: {Object}}} req + * @param {*} res + * @return {JSON} Success or error status + * @description + * Change a travel's information based on the trave;'s mongoID specified in req.params.id. + * The id is moved to req.body.id from req.params.id by validation. + * Returns a 200 status for an updated travel. + * The new information is located in req.body. + */ +function updatedTravel(req, res) { + return res.status(200).json({ + message: Constants.Success.TRAVEL_UPDATE, + data: req.body + }); +} + +module.exports = { + okay: okay, + showTravel: showTravel, + updatedTravel: updatedTravel, + createdTravel: createdTravel +}; diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index 06b65ce6..5228f51d 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -7,6 +7,7 @@ const Services = { Storage: require("../services/storage.service"), Email: require("../services/email.service"), Account: require("../services/account.service"), + Travel: require("../services/travel.service"), Env: require("../services/env.service") }; const Middleware = { @@ -158,9 +159,9 @@ async function validateConfirmedStatusFromHackerId(req, res, next) { const hacker = await Services.Hacker.findById(req.params.id); if (hacker == null) { return next({ - status: 404, - message: Constants.Error.HACKER_404_MESSAGE, - data: req.body.hackerId + status: 404, + message: Constants.Error.HACKER_404_MESSAGE, + data: req.body.hackerId }); } const account = await Services.Account.findById(hacker.accountId); @@ -235,7 +236,7 @@ function ensureAccountLinkedToHacker(req, res, next) { hacker && req.user && String.toString(hacker.accountId) === - String.toString(req.user.id) + String.toString(req.user.id) ) { return next(); } else { @@ -545,6 +546,12 @@ async function updateHacker(req, res, next) { }); } req.email = acct.email; + + // If this hacker has a travel account associated with it, then update request to reflect amount wanted for travel + const travel = await Services.Travel.findByHackerId(hacker.id); + if (travel) { + await Services.Travel.updateOne(travel.id, { "request": hacker.application.accommodation.travel }); + } return next(); } else { return next({ @@ -617,7 +624,7 @@ function parseAcceptEmail(req, res, next) { /** - * @function createhacker + * @function createHacker * @param {{body: {hackerDetails: object}}} req * @param {*} res * @param {(err?)=>void} next diff --git a/middlewares/travel.middleware.js b/middlewares/travel.middleware.js new file mode 100644 index 00000000..ad268b3f --- /dev/null +++ b/middlewares/travel.middleware.js @@ -0,0 +1,247 @@ +"use strict"; + +const TAG = `[ TRAVEL.MIDDLEWARE.js ]`; +const mongoose = require("mongoose"); +const Services = { + Travel: require("../services/travel.service"), + Hacker: require("../services/hacker.service"), + Account: require("../services/account.service"), +}; +const Middleware = { + Util: require("./util.middleware") +}; +const Constants = { + General: require("../constants/general.constant"), + Error: require("../constants/error.constant") +}; + +/** + * @function parsePatch + * @param {body: {id: ObjectId}} req + * @param {*} res + * @param {(err?) => void} next + * @return {void} + * @description Delete the req.body.id that was added by the validation of route parameter. + */ +function parsePatch(req, res, next) { + delete req.body.id; + return next(); +} + + +/** + * @function parseTravel + * @param {{body: {accountId: ObjectId, hackerId: ObjectId, authorization: string}}} req + * @param {*} res + * @param {(err?)=>void} next + * @return {void} + * @description + * Moves accountId & hackerId from req.body to req.body.travelId. + * Adds _id to hackerDetails. + */ +function parseTravel(req, res, next) { + const travelDetails = { + _id: mongoose.Types.ObjectId(), + accountId: req.body.accountId, + hackerId: req.body.hackerId + }; + req.body.token = req.body.authorization; + + delete req.body.accountId; + delete req.body.hackerId; + + req.body.travelDetails = travelDetails; + + return next(); +} + +/** + * @function addRequestFromHacker + * @param {{body: {travelDetails: {request: Number}}}} req + * @param {JSON} res + * @param {(err?)=>void} next + * @return {void} + * @description + * Load travel request from hacker application and add it to + * req.body.travelDetails + */ +async function addRequestFromHacker(req, res, next) { + const hacker = await Services.Hacker.findById(req.body.travelDetails.accountId); + if (!hacker) { + return next({ + status: 500, + message: Constants.Error.HACKER_UPDATE_500_MESSAGE, + data: { + hackerId: hacker.id, + accountId: hacker.accountId + } + }); + } + req.body.travelDetails.request = hacker.application.accommodation.travel; + return next(); +} + +/** + * @function addDefaultStatusAndOffer + * @param {{body: {travelDetails: {status: String, offer: Number}}}} req + * @param {JSON} res + * @param {(err?)=>void} next + * @return {void} + * @description Adds default status and offer to travelDetails. + */ +function addDefaultStatusAndOffer(req, res, next) { + req.body.travelDetails.status = "None"; + req.body.travelDetails.offer = 0; + return next(); +} + +/** + * @function createTravel + * @param {{body: {hackerTravel: object}}} req + * @param {*} res + * @param {(err?)=>void} next + * @return {void} + * @description + * Creates travel document after making sure there is no other hacker with the same linked accountId or hackerId + */ +async function createTravel(req, res, next) { + const travelDetails = req.body.travelDetails; + + const exists = await Services.Travel.findByAccountId( + travelDetails.accountId + ); + + if (exists) { + return next({ + status: 422, + message: Constants.Error.ACCOUNT_DUPLICATE_422_MESSAGE, + data: { + id: travelDetails.accountId + } + }); + } + const travel = await Services.Travel.createTravel(travelDetails); + if (!!travel) { + req.body.travel = travel; + return next(); + } else { + return next({ + status: 500, + message: Constants.Error.TRAVEL_CREATE_500_MESSAGE, + data: {} + }); + } +} + +/** + * Updates a travel that is specified by req.params.id + * @param {{params:{id: string}, body: *}} req + * @param {*} res + * @param {*} next + */ +async function updateTravel(req, res, next) { + const travel = await Services.Travel.updateOne(req.params.id, req.body); + if (travel) { + return next(); + } else { + return next({ + status: 404, + message: Constants.Error.TRAVEL_404_MESSAGE, + data: { + id: req.params.id + } + }); + } +} + +/** + * @async + * @function findById + * @param {{body: {id: ObjectId}}} req + * @param {*} res + * @description Retrieves a travel's information via req.body.id, moving result to req.body.travel if succesful. + */ +async function findById(req, res, next) { + const travel = await Services.Travel.findById(req.body.id); + + if (!travel) { + return next({ + status: 404, + message: Constants.Error.TRAVEL_404_MESSAGE + }); + } + + req.body.travel = travel; + next(); +} + +async function findByEmail(req, res, next) { + const account = await Services.Account.findByEmail(req.body.email); + if (!account) { + return next({ + status: 404, + message: Constants.Error.ACCOUNT_404_MESSAGE, + error: {} + }); + } + const travel = await Services.Travel.findByAccountId(account._id); + if (!travel) { + return next({ + status: 404, + message: Constants.Error.TRAVEL_404_MESSAGE, + error: {} + }); + } + + req.body.travel = travel; + next(); +} + +/** + * Finds the travel information of the logged in user + * and places that information in req.body.travel + * @param {{user: {id: string}}} req + * @param {*} res + * @param {(err?)=>void} next + */ +async function findSelf(req, res, next) { + if ( + req.user.accountType != Constants.General.HACKER || + !req.user.confirmed + ) { + return next({ + status: 409, + message: Constants.Error.ACCOUNT_TYPE_409_MESSAGE, + error: { + id: req.user.id + } + }); + } + + const travel = await Services.Travel.findByAccountId(req.user.id); + + if (!!travel) { + req.body.travel = travel; + return next(); + } else { + return next({ + status: 409, + message: Constants.Error.TRAVEL_404_MESSAGE, + error: { + id: req.user.id + } + }); + } +} + +module.exports = { + parsePatch: parsePatch, + parseTravel: parseTravel, + addDefaultStatusAndOffer: addDefaultStatusAndOffer, + addRequestFromHacker: Middleware.Util.asyncMiddleware(addRequestFromHacker), + createTravel: Middleware.Util.asyncMiddleware(createTravel), + updateTravel: Middleware.Util.asyncMiddleware(updateTravel), + findById: Middleware.Util.asyncMiddleware(findById), + findByEmail: Middleware.Util.asyncMiddleware(findByEmail), + findSelf: Middleware.Util.asyncMiddleware(findSelf) +}; diff --git a/middlewares/validators/travel.validator.js b/middlewares/validators/travel.validator.js new file mode 100644 index 00000000..9f9c768c --- /dev/null +++ b/middlewares/validators/travel.validator.js @@ -0,0 +1,35 @@ +"use strict"; +const VALIDATOR = require("./validator.helper"); +const Constants = require("../../constants/general.constant"); + +module.exports = { + newTravelValidator: [ + VALIDATOR.integerValidator("body", "request", false, 0, 3000), + VALIDATOR.jwtValidator( + "header", + "token", + process.env.JWT_CONFIRM_ACC_SECRET, + true + ) + ], + updateTravelValidator: [ + VALIDATOR.integerValidator("body", "request", false, 0, 3000) + ], + updateStatusValidator: [ + VALIDATOR.enumValidator( + "body", + "status", + Constants.TRAVEL_STATUSES, + false + ) + ], + updateOfferValidator: [ + VALIDATOR.integerValidator( + "body", + "offer", + false, + 0, + 3000 + ) + ] +}; diff --git a/models/travel.model.js b/models/travel.model.js new file mode 100644 index 00000000..8e7999e4 --- /dev/null +++ b/models/travel.model.js @@ -0,0 +1,41 @@ +"use strict"; +const Constants = require("../constants/general.constant"); +const mongoose = require("mongoose"); +//describes the data type +const TravelSchema = new mongoose.Schema({ + accountId: { // The account this travel data is associated with + type: mongoose.Schema.Types.ObjectId, + ref: "Account", + required: true + }, + hackerId: { // The hacker this travel data is associated with + type: mongoose.Schema.Types.ObjectId, + ref: "Hacker", + required: true + }, + status: { // Has this hacker been approved for funds, etc. + type: String, + enum: Constants.TRAVEL_STATUSES, + required: true, + default: "None" + }, + request: { // Amount of money hacker has requested for travel + type: Number, + required: true + }, + offer: { // Amount of money we have offered hacker for travel + type: Number, + default: 0 + } +}); + +TravelSchema.methods.toJSON = function () { + const hs = this.toObject(); + delete hs.__v; + hs.id = hs._id; + delete hs._id; + return hs; +}; + +//export the model +module.exports = mongoose.model("Travel", TravelSchema); diff --git a/routes/api/travel.js b/routes/api/travel.js new file mode 100644 index 00000000..41a8994c --- /dev/null +++ b/routes/api/travel.js @@ -0,0 +1,249 @@ +"use strict"; +const express = require("express"); +const Controllers = { + Travel: require("../../controllers/travel.controller") +}; +const Middleware = { + Validator: { + /* Insert the require statement to the validator file here */ + Travel: require("../../middlewares/validators/travel.validator"), + RouteParam: require("../../middlewares/validators/routeParam.validator") + }, + /* Insert all of ther middleware require statements here */ + parseBody: require("../../middlewares/parse-body.middleware"), + Util: require("../../middlewares/util.middleware"), + Travel: require("../../middlewares/travel.middleware"), + Hacker: require("../../middlewares/hacker.middleware"), + Auth: require("../../middlewares/auth.middleware"), + //Search: require("../../middlewares/search.middleware") +}; +const Services = { + Travel: require('../../services/travel.service'), + Hacker: require("../../services/hacker.service"), + Account: require("../../services/account.service") +}; +const CONSTANTS = require("../../constants/general.constant"); + +module.exports = { + activate: function (apiRouter) { + const travelRouter = express.Router(); + + + travelRouter.route("/").get( + Controllers.Travel.okay + ) + + /** + * @api {get} /travel/self get information about own hacker's travel + * @apiName self + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + "message": "Travel found by logged in account id", + "data": { + "id":"5bff4d736f86be0a41badb91", + "status": "Claimed" + "request": 90, + "offer": 80 + } + } + + * @apiError {string} message Error message + * @apiError {object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Travel not found", "data": {}} + */ + travelRouter.route("/self").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), + + Middleware.Travel.findSelf, + Controllers.Travel.showTravel + ); + + /** + * @api {get} /travel/:id get a traveler's information + * @apiName getTravel + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiParam (param) {String} id a travel's unique mongoID + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + "message": "Successfully retrieved travel information", + "data": { + "id":"5bff4d736f86be0a41badb91", + "status": "Valid", + "request": 100, + "offer": 50 + } + } + + * @apiError {String} message Error message + * @apiError {Object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Travel not found", "data": {}} + */ + travelRouter.route("/:id").get( + Middleware.Validator.RouteParam.idValidator, + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Hacker.findByAccountId]), + + Middleware.parseBody.middleware, + + Middleware.Travel.findById, + Controllers.Travel.showTravel + ); + + /** + * @api {get} /travel/email/:email get a travel's information + * @apiName getTravel + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiParam (param) {String} email a travel's unique email + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + "message": "Successfully retrieved travel information", + "data": { + "id":"5bff4d736f86be0a41badb91", + "status": "Valid", + "request": 100, + "offer": 50 + } + } + + * @apiError {String} message Error message + * @apiError {Object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Travel not found", "data": {}} + */ + travelRouter.route("/email/:email").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Account.findByEmail]), + + Middleware.Validator.RouteParam.emailValidator, + Middleware.parseBody.middleware, + + Middleware.Travel.findByEmail, + Controllers.Travel.showTravel + ); + + /** + * @api {post} /travel/ create a new travel + * @apiName createTravel + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiParam (body) {MongoID} accountId ObjectID of the respective account + * @apiParam (body) {MongoID} hackerId ObjectID of the respective hacker + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Travel creation successful", + * "data": { + * "id":"5bff4d736f86be0a41badb91", + * "status": "None", + * "request": 50, + * "offer": 0 + * } + * } + + * @apiError {string} message Error message + * @apiError {object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Error while creating travel", "data": {}} + */ + travelRouter.route("/").post( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), + Middleware.Validator.Travel.newTravelValidator, + + Middleware.parseBody.middleware, + // validate type + Middleware.Hacker.validateConfirmedStatusFromAccountId, + + Middleware.Travel.parseTravel, + + Middleware.Travel.addRequestFromHacker, + Middleware.Travel.addDefaultStatusAndOffer, + Middleware.Travel.createTravel, + + Controllers.Travel.createdTravel + ); + + /** + * @api {patch} /travel/status/:id update a traveler's status + * @apiName patchTravelStatus + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiParam (body) {string} [status] Status of the travel's reimbursement ("None"|"Bus"|"Offered"|"Valid"|"Invalid"|"Claimed") + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Changed travel information", + * "data": { + * "status": "Accepted" + * } + * } + * @apiPermission Administrator + */ + travelRouter.route("/status/:id").patch( + Middleware.Validator.RouteParam.idValidator, + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Travel.findById]), + Middleware.Validator.Travel.updateStatusValidator, + Middleware.parseBody.middleware, + Middleware.Travel.parsePatch, + + Middleware.Travel.updateTravel, + Controllers.Travel.updatedTravel + ); + + /** + * @api {patch} /travel/offer/:id update a traveler's offer + * @apiName patchTravelOffer + * @apiGroup Travel + * @apiVersion 2.0.1 + * + * @apiParam (body) {number} [offer] Amount of money offered for travel + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Travel object + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Changed travel information", + * "data": { + * "offer": 75 + * } + * } + * @apiPermission Administrator + */ + travelRouter.route("/offer/:id").patch( + Middleware.Validator.RouteParam.idValidator, + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Travel.findById]), + Middleware.Validator.Travel.updateOfferValidator, + Middleware.parseBody.middleware, + Middleware.Travel.parsePatch, + + Middleware.Travel.updateTravel, + Controllers.Travel.updatedTravel + ); + + apiRouter.use("/travel", travelRouter); + } +}; diff --git a/scripts/batch_update_travel.js b/scripts/batch_update_travel.js new file mode 100644 index 00000000..4d69ba1c --- /dev/null +++ b/scripts/batch_update_travel.js @@ -0,0 +1,10 @@ +// use hackboard-dev; // Change to product for actual update + +// Create a travel document for every hacker document +db.hackers.find().forEach(hacker => { + let request = 0; + if (hacker.application && hacker.application.accommodation && hacker.application.accommodation.travel) { + request = hacker.application.accommodation.travel; + } + db.travels.insert({ hackerId: hacker._id, accountId: hacker.accountId, request: request, offer: 0, status: "None" }) +}); diff --git a/services/hacker.service.js b/services/hacker.service.js index b6b85c8e..cc943582 100644 --- a/services/hacker.service.js +++ b/services/hacker.service.js @@ -198,7 +198,7 @@ function getStats(hackers) { stats.graduationYear[hacker.application.general.graduationYear] = stats .graduationYear[hacker.application.general.graduationYear] ? stats.graduationYear[hacker.application.general.graduationYear] + - 1 + 1 : 1; for (const dietaryRestrictions of hacker.accountId .dietaryRestrictions) { diff --git a/services/travel.service.js b/services/travel.service.js new file mode 100644 index 00000000..e9a18c6f --- /dev/null +++ b/services/travel.service.js @@ -0,0 +1,112 @@ +"use strict"; +const Travel = require("../models/travel.model"); +const logger = require("./logger.service"); + +// const Constants = require("../constants/general.constant"); + + +/** + * @function createTravel + * @param {{_id: ObjectId, accountId: ObjectId, status: enum of Constants.TRAVEL_STATUSES, request: Number, offer?: number}} travelDetails + * @return {Promise} The promise will resolve to a travel object if save is successful. + * @description Adds a new travel to database. + */ +function createTravel(travelDetails) { + const TAG = `[Travel Service # createTravel]:`; + + const travel = new Travel(travelDetails); + + return travel.save(); +} + +/** + * @function updateOne + * @param {ObjectId} id + * @param {{_id?: ObjectId, accountId?: ObjectId, status?: enum of Constants.TRAVEL_STATUSES, request?: Number, offer?: number}} travelDetails + * @return {DocumentQuery} The document query will resolve to travel or null. + * @description Update an travel specified by its mongoId with information specified by travelDetails. + */ +function updateOne(id, travelDetails) { + const TAG = `[Travel Service # update ]:`; + + const query = { + _id: id + }; + + return Travel.findOneAndUpdate( + query, + travelDetails, + logger.updateCallbackFactory(TAG, "travel") + ); +} + +/** + * @function findById + * @param {ObjectId} id + * @return {DocumentQuery} The document query will resolve to travel or null. + * @description Finds an travel by the id, which is the mongoId. + */ +function findById(id) { + const TAG = `[Travel Service # findById ]:`; + + return Travel.findById(id, logger.queryCallbackFactory(TAG, "travel", id)); +} + +/** + * @async + * @function findOne + * @param {JSON} query + * @return {Travel | null} either travel or null + * @description Finds an travel by some query. + */ +async function findIds(queries) { + const TAG = `[Travel Service # findIds ]:`; + let ids = []; + + for (const query of queries) { + let currId = await Travel.findOne( + query, + "_id", + logger.queryCallbackFactory(TAG, "travel", query) + ); + ids.push(currId); + } + return ids; +} + +/** + * @function findByAccountId + * @param {ObjectId} accountId + * @return {DocumentQuery} A travel document queried by accountId + */ +function findByAccountId(accountId) { + const TAG = `[ Travel Service # findByAccountId ]:`; + const query = { + accountId: accountId + }; + + return Travel.findOne(query, logger.updateCallbackFactory(TAG, "travel")); +} + +/** + * @function findByHackerId + * @param {ObjectId} travelId + * @return {DocumentQuery} A travel document queried by hackerId + */ +function findByHackerId(hackerId) { + const TAG = `[ Travel Service # findByAccountId ]:`; + const query = { + hackerId: hackerId + }; + + return Travel.findOne(query, logger.updateCallbackFactory(TAG, "travel")); +} + +module.exports = { + createTravel: createTravel, + findById: findById, + updateOne: updateOne, + findIds: findIds, + findByAccountId: findByAccountId, + findByHackerId: findByHackerId +};