Skip to content

Commit

Permalink
feature(request): manager reject trip request
Browse files Browse the repository at this point in the history
- add request validation schema to validate the request Id
- add request middleware to verify manager
- add a controller to reject a trip request

[Finishes #167727738]
  • Loading branch information
kola-1 committed Sep 3, 2019
1 parent aea2a9b commit 5f78670
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 41 deletions.
1 change: 0 additions & 1 deletion .sequelizerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require('@babel/register');

const path = require('path');

module.exports = {
Expand Down
39 changes: 36 additions & 3 deletions src/controllers/requestController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { findById } from '../services/userServices';
import { createNotification } from '../services/notificationServices';

const { Request, Subrequest } = models;
const { serverError, unauthorizedUserRequest, noRequests } = messages;
const { create, getAll, bulkCreate } = DbServices;
const {
serverError, unauthorizedUserRequest, noRequests, rejectedTripRequest
} = messages;
const {
create, getAll, bulkCreate, update
} = DbServices;

/**
* request trip controller
Expand Down Expand Up @@ -119,8 +123,37 @@ const searchRequest = async (req, res) => {
}
};

/**
* reject request controller
* @param {Object} req - server request
* @param {Object} res - server response
* @returns {Object} - custom response
*/
const updateApprovalStatus = async (req, res) => {
try {
let approvalStatusValue, approvalStatusMessage;
const { requestId } = req.params;
const options = { returning: true, where: { id: requestId } };
const action = await req.url.match(/\/requests\/([a-z]+).*/);
if (action[1] === 'reject') {
approvalStatusValue = 'rejected';
approvalStatusMessage = rejectedTripRequest;
}
const updateColumn = { approvalStatus: approvalStatusValue };
await update(Request, updateColumn, options);
return response(res, 201, 'success', {
message: approvalStatusMessage
});
} catch (error) {
return response(res, 500, 'error', {
message: serverError,
});
}
};

export default {
requestTrip,
getUserRequest,
searchRequest
searchRequest,
updateApprovalStatus
};
4 changes: 2 additions & 2 deletions src/database/migrations/20190815205048-create-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ module.exports = {
allowNull: true,
type: Sequelize.BOOLEAN,
defaultValue: true
},
})
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
Expand Down
11 changes: 7 additions & 4 deletions src/database/migrations/20190820173619-create-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,24 @@ module.exports = {
type: Sequelize.STRING,
},
approvalStatus: {
type: Sequelize.BOOLEAN,
allowNull: false,
type: Sequelize.ENUM,
allowNull: true,
values: ['accepted', 'rejected']
},
multiCity: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
createdAt: {
allowNull: false,
allowNull: true,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
allowNull: false,
allowNull: true,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('NOW'),
},
});
},
Expand Down
29 changes: 20 additions & 9 deletions src/database/seeders/20190819153902-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import { hashPassword } from '../../utils/authHelper';
export default {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('Users', [
{
id: '38eb202c-3f67-4eed-b7ac-9c31bc226e0c',
firstName: 'line',
lastName: 'manager',
email: 'linemanager@gmail.com',
password: hashPassword('Password'),
phoneNo: '2347033545645',
verified: true
},
{
id: '122a0d86-8b78-4bb8-b28f-8e5f7811c456',
firstName: 'James',
lastName: 'Williams',
email: 'jammy@gmail.com',
phoneNo: '2347032123304',
password: hashPassword('jammy11167'),
createdAt: new Date(),
updatedAt: new Date(),
verified: true,
roleId: roles.SUPER_ADMIN,
},
Expand All @@ -23,8 +30,6 @@ export default {
email: 'samuelman@gmail.com',
password: hashPassword('samman12358'),
phoneNo: null,
createdAt: new Date(),
updatedAt: new Date(),
verified: false,
roleId: roles.MANAGER,
}, {
Expand All @@ -34,8 +39,6 @@ export default {
email: 'polman@gmail.com',
password: hashPassword('polly11167'),
phoneNo: '2347032123404',
createdAt: new Date(),
updatedAt: new Date(),
verified: true,
roleId: roles.REQUESTER,
}, {
Expand All @@ -45,11 +48,19 @@ export default {
email: 'freeman@gmail.com',
password: hashPassword('polly123456'),
phoneNo: '2347032123409',
createdAt: new Date(),
updatedAt: new Date(),
verified: true,
lineManager: '0ce36391-2c08-4703-bddb-a4ea8cccbbc5',
roleId: roles.REQUESTER,
verified: true
},
{
id: '2999c776-0f6f-471d-bced-b661d6e75586',
firstName: 'request',
lastName: 'man',
email: 'requestman@gmail.com',
password: hashPassword('requestman'),
phoneNo: '2347032746854',
lineManager: '38eb202c-3f67-4eed-b7ac-9c31bc226e0c',
verified: true
}
], {});
},
Expand Down
26 changes: 16 additions & 10 deletions src/database/seeders/20190826210429-demo-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ export default {
destinationCity: 'Istanbul',
userId: '122a0d86-8b78-4bb8-b28f-8e5f7811c456',
departureDate: '01-07-2017',
createdAt: new Date(),
updatedAt: new Date(),
returnDate: '02-08-2017',
reason: 'Check stocks',
accommodation: 'Great Istanbul Arena',
approvalStatus: false
approvalStatus: 'rejected'
},
{
id: 'fb94de4d-47ff-4079-89e8-b0186c0a3be8',
Expand All @@ -22,25 +20,33 @@ export default {
destinationCity: 'Lagos',
departureDate: '01-07-2017',
userId: 'fb94de4d-47ff-4079-89e8-b0186c0a3be8',
createdAt: new Date(),
updatedAt: new Date(),
returnDate: '02-08-2017',
reason: 'Annual meeting',
accommodation: 'Eko Hotels & Suites',
approvalStatus: true
}, {
approvalStatus: 'accepted'
},
{
id: '0ce36391-2c08-4703-bddb-a4ea8cccbbc5',
type: 'return',
originCity: 'Abuja',
destinationCity: 'Lagos',
departureDate: '01-07-2018',
userId: '122a0d86-8b78-4bb8-b28f-8e5f7811c456',
createdAt: new Date(),
updatedAt: new Date(),
returnDate: '02-08-2018',
reason: 'Annual meeting',
accommodation: 'Eko Hotels & Suites',
approvalStatus: true
approvalStatus: 'accepted'
},
{
id: 'b2092fb0-502a-4105-961f-2d310d340168',
userId: '2999c776-0f6f-471d-bced-b661d6e75586',
type: 'return',
originCity: 'lagos',
destinationCity: 'bahamas',
departureDate: '2019-09-21 17:59:04.305+00',
returnDate: '2020-08-21 17:59:04.305+00',
reason: 'vacation',
accommodation: 'Hotel Transylvania',
}
], {});
},
Expand Down
35 changes: 35 additions & 0 deletions src/middlewares/requestMiddlewares.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import response from '../utils/response';
import messages from '../utils/messages';
import DbServices from '../services/dbServices';
import models from '../models';

const { User, Request } = models;
const { findOneIncludeModel } = DbServices;

const verifyRequestLineManager = async (req, res, next) => {
try {
const { id: loggedInUserId } = req.decoded;
const { requestId } = req.params;
const table2 = {
model: User,
alias: 'User',
column: { lineManager: loggedInUserId }
};
const data = await findOneIncludeModel(Request, requestId, table2);

const requestLineManagerId = data.User.lineManager;
const manager = loggedInUserId === requestLineManagerId;
if (!manager) {
return response(res, 401, 'error', {
message: messages.unauthorized
});
}
next();
} catch (error) {
return response(res, 500, 'error', {
errors: error
});
}
};

export default verifyRequestLineManager;
6 changes: 3 additions & 3 deletions src/models/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING,
},
approvalStatus: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
type: DataTypes.ENUM,
allowNull: true,
values: ['accepted', 'rejected']
},
multiCity: {
type: DataTypes.BOOLEAN,
Expand Down
75 changes: 73 additions & 2 deletions src/routes/api/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import validate from '../../middlewares/validator';
import requestSchema from '../../validation/requestSchema';
import { checkToken, checkUserId } from '../../middlewares/userMiddlewares';
import checkBlacklist from '../../middlewares/blacklistMiddleware';
import verifyRequestLineManager from '../../middlewares/requestMiddlewares';

const { requestTrip, getUserRequest, searchRequest } = requestController;
const { requestTripSchema, getUserRequestSchema, searchRequestTripSchema } = requestSchema;
const {
requestTrip, getUserRequest, searchRequest, updateApprovalStatus
} = requestController;
const {
requestTripSchema, getUserRequestSchema, searchRequestTripSchema, requestIdSchema
} = requestSchema;

const requestRoute = (router) => {
router.route('/requests')
Expand Down Expand Up @@ -122,6 +127,7 @@ const requestRoute = (router) => {
* - bearerAuth: []
*/
.post(checkToken, checkBlacklist, validate(requestTripSchema), requestTrip);

router.route('/requests/user/:userId')
/**
* @swagger
Expand Down Expand Up @@ -281,6 +287,71 @@ const requestRoute = (router) => {
* - bearerAuth: []
*/
.post(checkToken, validate(searchRequestTripSchema), searchRequest);

router.route('/requests/reject/:requestId')
/**
* @swagger
* components:
* schemas:
* acceptOrRejectTrip:
* properties:
* message:
* type: string
* readOnly: true
* ErrorResponse:
* properties:
* status:
* type: string
* example: error
* data:
* type: string
*/

/**
* @swagger
* /api/v1/requests/reject/{requestId}:
* patch:
* tags:
* - Requests
* description: Reject a request for a trip
* parameters:
* - in: path
* name: requestId
* schema:
* type: string
* required: true
* produces:
* - application/json
* responses:
* 201:
* description: Trip request successfully rejected
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: success
* data:
* allOf:
* - $ref: '#/components/schemas/acceptOrRejectTrip'
* 400:
* description: Input validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* security:
* - bearerAuth: []
*/
.patch(checkToken, validate(requestIdSchema), verifyRequestLineManager, updateApprovalStatus);
};

export default requestRoute;
26 changes: 26 additions & 0 deletions src/services/dbServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ const DbServices = {
*/
bulkCreate(model, data) {
return model.bulkCreate(data, { returning: true });
},

/**
* @param {object} model primary model /table
* @param {string} id the id (primary model /table)
* @param {object} modelDetailsToInclude object containing 3 properties (secondary model /table)
* @param {object} modelDetailsToInclude.model secondary model /table
* @param {string} modelDetailsToInclude.alias alias of secondary model /table
* @param {object} modelDetailsToInclude.column column of secondary model /table
* @returns {Promise} Promise resolved or rejected
* @description gets data from a primary table and also joins data from a secondary table
*
* @example
* getTwoTables(Request, 1, { model: User, alias: 'User', column: { lineManager: managerId } })
*/
findOneIncludeModel(model, id, modelDetailsToInclude) {
return model.findOne({
where: { id },
include: [
{
model: modelDetailsToInclude.model,
as: modelDetailsToInclude.alias,
where: modelDetailsToInclude.column
}
]
});
}
};

Expand Down
Loading

0 comments on commit 5f78670

Please sign in to comment.