Skip to content

Commit

Permalink
feature(update-travel): implement update travel request feature
Browse files Browse the repository at this point in the history
- write tests
- add input validations
- add route and controller logic

[Finishes #167891600]
  • Loading branch information
max-wel committed Sep 5, 2019
1 parent 8e32428 commit 01a9a9b
Show file tree
Hide file tree
Showing 9 changed files with 742 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@sendgrid/mail": "^6.4.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.4",
"date-fns": "^2.0.1",
"dotenv": "^6.2.0",
"ejs": "^2.6.1",
"errorhandler": "^1.5.0",
Expand Down
68 changes: 68 additions & 0 deletions src/controllers/Trip.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,74 @@ class TripController {
return res.status(response.code).json(response);
}
}

/**
*
* @param {object} req Request
* @param {object} res Response
* @returns {object} JSON
*/
static async editTripRequest(req, res) {
try {
const { tripId } = req.params;
const { payload: { id: userId } } = req.payload;
const {
from, departureDate, returnDate, destinations, reason
} = req.body;
// Check if trip exists
const trip = await Trip.findOne(
{
where: { id: tripId, userId },
raw: true
}
);
if (!trip) {
const response = new Response(false, 404, 'Trip does not exist');
return res.status(response.code).json(response);
}
// check if trip is still open
if (trip.status !== 'pending') {
const response = new Response(false, 403, 'This trip is no longer open');
return res.status(response.code).json(response);
}
// update record in Trip table
const [, [updatedTrip]] = await Trip.update(
{
startBranchId: from,
tripDate: departureDate,
returnDate: trip.type === 'return' ? returnDate : null,
reason,
},
{ where: { id: tripId }, returning: true, raw: true }
);

// update records in Stops table
const modifiedDestinations = destinations.map((destination) => Stop.update(
{
destinationBranchId: destination.to,
accomodationId: destination.accomodation,
},
{ where: { id: destination.id }, returning: true, plain: true }
));
const result = await Promise.all(modifiedDestinations);
const updatedStops = result.map((item) => item[1]);

const response = new Response(
false,
200,
'Travel request successfully updated',
{ trip: { ...updatedTrip, stops: updatedStops } }
);
return res.status(response.code).json(response);
} catch (error) {
const response = new Response(
false,
500,
'Server error, Please try again later'
);
return res.status(response.code).json(response);
}
}
}

export default TripController;
5 changes: 5 additions & 0 deletions src/database/seeders/20190829125545-create-branches.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ module.exports = {
id: '3dd3b34a-7554-455e-a688-36afda199624',
name: 'Gwagwalada',
locationId: '0190ae78-d184-4258-add5-0b2c6982efef'
},
{
id: '1bf9f868-2a59-45ae-8f68-ad68095ca0ac',
name: 'Asokoro',
locationId: '0190ae78-d184-4258-add5-0b2c6982efef'
}
]),
down: queryInterface => queryInterface.bulkDelete('Branches', null, {})
Expand Down
39 changes: 38 additions & 1 deletion src/database/seeders/20190829125546-create-trips.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,44 @@ module.exports = {
returnDate: null,
reason: 'to meet with clients',
status: 'approved'
}
},
{
id: '72a5c8f5-27eb-4623-b6f4-09f77cc871f3',
type: 'oneway',
startBranchId: '3dd3b34a-7554-425e-a688-36afda199614',
userId: '91542e6f-94bc-4e80-a667-586fb0752f24',
tripDate: '2019-02-17',
reason: 'for holiday',
status: 'pending'
},
{
id: 'ba3f6b93-f09e-49a4-bb28-c65555250bc1',
type: 'return',
startBranchId: '3dd3b34a-7554-425e-a688-36afda199614',
userId: '91542e6f-94bc-4e80-a667-586fb0752f24',
tripDate: '2019-02-17',
returnDate: '2019-02-23',
reason: 'for holiday',
status: 'pending'
},
{
id: 'db17ddb2-a6ba-49f5-9715-e0e81eb7720a',
type: 'multiple',
startBranchId: '3dd3b34a-7554-425e-a688-36afda199614',
userId: '91542e6f-94bc-4e80-a667-586fb0752f24',
tripDate: '2019-02-17',
reason: 'for holiday',
status: 'pending'
},
{
id: '4ae4fef9-8e5e-4d2a-879a-a0425cd3d5aa',
type: 'oneway',
startBranchId: '3dd3b34a-7554-425e-a688-36afda199614',
userId: '91542e6f-94bc-4e80-a667-586fb0752f24',
tripDate: '2019-02-17',
reason: 'for holiday',
status: 'approved'
},
]),
down: queryInterface => queryInterface.bulkDelete('Trips', null, {})
};
24 changes: 24 additions & 0 deletions src/database/seeders/20190829130101-create-stops.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ module.exports = {
destinationBranchId: '3dd3b34a-7554-455e-a688-36afda199624',
accomodationId: '3dd3b34a-7554-425e-c688-36afda199619',
tripId: 'ffe25dbe-49ea-4759-8462-ed116f6749df'
},
{
id: 'd3d9c312-6ac0-4f9a-a371-efa587e8f56c',
destinationBranchId: '3dd3b34a-7554-455e-a688-36afda199624',
accomodationId: '3dd3b34a-7554-425e-c688-36afda199619',
tripId: '72a5c8f5-27eb-4623-b6f4-09f77cc871f3'
},
{
id: 'a041b2c9-eaba-4d8d-87b1-378d557b2678',
destinationBranchId: '3dd3b34a-7554-455e-a688-36afda199624',
accomodationId: '3dd3b34a-7554-425e-c688-36afda199619',
tripId: 'ba3f6b93-f09e-49a4-bb28-c65555250bc1'
},
{
id: '13020a26-5840-4758-93bd-9da91920d1e8',
destinationBranchId: '3dd3b34a-7554-455e-a688-36afda199624',
accomodationId: '3dd3b34a-7554-425e-c688-36afda199619',
tripId: 'db17ddb2-a6ba-49f5-9715-e0e81eb7720a'
},
{
id: '79d09466-ee4f-4e96-a38b-009e4595121e',
destinationBranchId: '1bf9f868-2a59-45ae-8f68-ad68095ca0ac',
accomodationId: '3dd3b34a-7554-425e-c688-36afda199619',
tripId: 'db17ddb2-a6ba-49f5-9715-e0e81eb7720a'
}
]),
down: queryInterface => queryInterface.bulkDelete('Stops', null, {})
Expand Down
8 changes: 7 additions & 1 deletion src/routes/trip.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Router } from 'express';
import TripController from '../controllers/Trip';
import Token from '../helpers/Token';
import validator from '../middlewares/validator';
import { oneWaySchema } from '../validation/tripSchema';
import { oneWaySchema, editTripRequestSchema } from '../validation/tripSchema';

const tripRoutes = Router();

Expand All @@ -13,5 +13,11 @@ tripRoutes.post(
validator(oneWaySchema),
TripController.createTripRequest
);
tripRoutes.put(
'/:tripId',
Token.verifyToken,
validator(editTripRequestSchema),
TripController.editTripRequest
);

export default tripRoutes;
104 changes: 101 additions & 3 deletions src/validation/tripSchema.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { body } from 'express-validator';
import { body, param } from 'express-validator';
import { isValid, parseISO } from 'date-fns';
import models from '../database/models';

const { Branch, Accomodation } = models;
const { Branch, Accomodation, Stop } = models;
const dateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;

const oneWaySchema = [
Expand Down Expand Up @@ -64,4 +65,101 @@ const oneWaySchema = [
}),
];

export { oneWaySchema };
const editTripRequestSchema = [
param('tripId')
.isUUID(4)
.withMessage('Invalid trip Id'),
body('from')
.not().isEmpty({ ignore_whitespace: true })
.withMessage('Starting point is required')
.trim()
.isUUID(4)
.withMessage('Invalid starting point')
.custom(async (value) => {
const branch = await Branch.findOne({ where: { id: value } });
if (!branch) {
throw new Error('Start branch location does not exist');
}
return true;
}),
body('departureDate')
.not().isEmpty({ ignore_whitespace: true })
.withMessage('Departure date is required')
.custom((value) => isValid(parseISO(value)))
.withMessage('Invalid departure date format')
.customSanitizer((value) => new Date(value)),
body('returnDate')
.optional()
.trim()
.custom((value, { req }) => {
const { departureDate } = req.body;
if (!isValid(parseISO(value))) {
throw new Error('Invalid return date format');
}
if (new Date(value) <= new Date(departureDate)) {
throw new Error('Return date must be greater than departure date');
}
return true;
})
.customSanitizer((value) => new Date(value)),
body('reason')
.not().isEmpty({ ignore_whitespace: true })
.withMessage('Travel reason is required')
.trim(),
body('destinations')
.exists()
.withMessage('Destination(s) is required')
.isArray()
.withMessage('Invalid destination(s) format'),
body('destinations.*.id')
.exists()
.withMessage('Destination Id is required')
.trim()
.isUUID(4)
.withMessage('Invalid destination Id format')
.custom(async (value) => {
const stop = await Stop.findOne({ where: { id: value } });
if (!stop) {
throw new Error('Destination does not exist');
}
return true;
}),
body('destinations.*.to')
.exists()
.withMessage('Destination is required')
.trim()
.isUUID(4)
.withMessage('Invalid destination format')
.custom(async (value, { req }) => {
const { destinations } = req.body;
const branch = await Branch.findOne({ where: { id: value } });
if (!branch) {
throw new Error('Destination branch does not exist');
}
if (value === req.body.from) {
throw new Error('Start and Destination branch should not be the same');
}
const filtered = destinations.filter(
(destination) => destination.to === value
);
if (filtered.length > 1) {
throw new Error('One or more destination branches are the same');
}
return true;
}),
body('destinations.*.accomodation')
.exists()
.withMessage('Accomodation is required')
.trim()
.isUUID(4)
.withMessage('Invalid accomodation format')
.custom(async (value) => {
const accomodation = await Accomodation.findOne({ where: { id: value } });
if (!accomodation) {
throw new Error('Accomodation does not exist');
}
return true;
}),
];

export { oneWaySchema, editTripRequestSchema };
Loading

0 comments on commit 01a9a9b

Please sign in to comment.