Skip to content
This repository has been archived by the owner on Jul 20, 2020. It is now read-only.

Commit

Permalink
feature(rate): a user can rate a facility
Browse files Browse the repository at this point in the history
- add the rateFacility controller
- add the totalRate and averageRate attribute to the facility table
- add the ratings table

[Maintains #170947578]
  • Loading branch information
izzett222 committed Mar 19, 2020
1 parent 080a930 commit bda13b1
Show file tree
Hide file tree
Showing 17 changed files with 402 additions and 4 deletions.
22 changes: 22 additions & 0 deletions src/controllers/facilitiesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import db from '../models';
import Response from '../utils/ResponseHandler';
import uploadImage from '../services/uploadImageService';
import findFacilityByLocation from '../utils/findFacility';
import FacilityService from '../services/facilityService';
import stringHelper from '../utils/stringHelper';
/**
* @description Facilities Controller
* @class FacilitiesController
Expand Down Expand Up @@ -192,5 +194,25 @@ class FacilitiesController {
return Response.errorResponse(res, 500, res.__('server error'));
}
}

/**
* @description rate facility method
* @static
* @param {Object} req
* @param {Object} res
* @returns {Object} Facility with ratings
* @memberof FacilitiesController
*/
static async rateFacility(req, res) {
const { user } = req;
const { facilityId } = req.params;
const { rating } = req.query;
try {
const result = await FacilityService.rateFacility(facilityId, user, rating);
return ((result === stringHelper.facilityNotFound) && Response.errorResponse(res, 404, res.__(result))) || ((result === stringHelper.notVisitedFacility) && Response.errorResponse(res, 403, res.__(result))) || ((typeof result === 'object') && Response.success(res, 200, res.__('facility rated'), result));
} catch (err) {
return Response.errorResponse(res, 500, err.message);
}
}
}
export default FacilitiesController;
10 changes: 10 additions & 0 deletions src/migrations/20200219142859-create-facilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ module.exports = {
createdBy: {
type: Sequelize.STRING
},
totalRating: {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false
},
averageRating: {
type: Sequelize.FLOAT,
defaultValue: 0,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
Expand Down
43 changes: 43 additions & 0 deletions src/migrations/20200313163016-create-ratings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Ratings', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.STRING
},
rating: {
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.STRING,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references:{
model: 'Users',
key: 'id'
}
},
facilityId: {
type: Sequelize.STRING,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references:{
model: 'Facilities',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Ratings');
}
};
2 changes: 2 additions & 0 deletions src/models/facilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module.exports = (sequelize, DataTypes) => {
amenities: DataTypes.STRING,
services: DataTypes.STRING,
likes: DataTypes.INTEGER,
totalRating: DataTypes.INTEGER,
averageRating: DataTypes.FLOAT,
likesId: DataTypes.ARRAY(DataTypes.STRING),
unlikes: DataTypes.INTEGER,
unlikesId: DataTypes.ARRAY(DataTypes.STRING)
Expand Down
20 changes: 20 additions & 0 deletions src/models/ratings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = (sequelize, DataTypes) => {
const Ratings = sequelize.define('Ratings', {
userId: DataTypes.STRING,
facilityId: DataTypes.STRING,
rating: DataTypes.INTEGER
}, {});
Ratings.associate = (models) => {
Ratings.belongsTo(models.User, {
foreignKey: 'userId',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
Ratings.belongsTo(models.Facilities, {
foreignKey: 'facilityId',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
};
return Ratings;
};
3 changes: 2 additions & 1 deletion src/routes/facilityRoute.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express from 'express';
import FacilitiesController from '../controllers/facilitiesController';
import protectRoute from '../middlewares/protectRoute';
import { createFacilityRules, bookingRules } from '../validation/validationRules';
import { createFacilityRules, bookingRules, rateQueryRules } from '../validation/validationRules';
import validationResult from '../validation/validationResult';
import { multerUploads } from '../utils/multer';
import alreadyLiked from '../utils/alreadyLiked';
Expand All @@ -14,5 +14,6 @@ router.post('/room', protectRoute.verifyUser, protectRoute.verifyTripAdminOrSupp
router.patch('/like', protectRoute.verifyUser, protectRoute.verifyFacility, protectRoute.checkIfLiked, alreadyUnliked, FacilitiesController.likeFacility);
router.patch('/unlike', protectRoute.verifyUser, protectRoute.verifyFacility, protectRoute.checkIfUnliked, alreadyLiked, FacilitiesController.unlikeFacility);
router.post('/book', protectRoute.verifyUser, protectRoute.verifyRequester, bookingRules, validationResult, FacilitiesController.bookFacility);
router.patch('/rate/:facilityId', protectRoute.verifyUser, rateQueryRules, validationResult, FacilitiesController.rateFacility);

export default router;
12 changes: 12 additions & 0 deletions src/seeders/20200212121323-User.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ module.exports = {
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '79660e6f-4b7d-4g21-81re-74f54jk91c8u',
firstName: 'jean',
lastName: 'shema',
email: 'jean@example.com',
password: bcrypt.hashSync('Bien@BAR789', Number(process.env.passwordHashSalt)),
isVerified: true,
role: 'requester',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '79660e6f-4b7d-4g21-81re-74f54e9e1c8a',
firstName: 'devrepubli',
Expand Down
18 changes: 18 additions & 0 deletions src/seeders/20200220082529-Rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ module.exports = {
availability: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '47d21452-ea54-4f2a-a1df-2c167c34cdd2',
facilityId: '5be72db7-5510-4a50-9f15-e23f103116d5',
roomName: 'Accra 2',
type: 'King-size',
availability: false,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '47d21452-ea54-4f2a-a1df-2c167c34cdd3',
facilityId: '5be72db7-5510-4a50-9f15-e23f103116d5',
roomName: 'Accra 3',
type: 'King-size',
availability: false,
createdAt: new Date(),
updatedAt: new Date(),
}
],
{},
Expand Down
20 changes: 20 additions & 0 deletions src/seeders/20200315125736-booking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
up: (queryInterface) => queryInterface.bulkInsert(
'Bookings', [
{
id: 'd1d6c6d3-92b3-4000-9f8e-70981c49dc6e',
roomId: '47d21452-ea54-4f2a-a1df-2c167c34cdd2',
facilityId: '5be72db7-5510-4a50-9f15-e23f103116d5',
checkin: '2018-01-11',
checkout: '2019-10-10',
requestId: '51e74db7-5510-4f50-9f15-e23710331ld5',
bookedBy: 'jdev@andela.com',
createdAt: new Date(),
updatedAt: new Date(),
}
],
{},
),

down: (queryInterface) => queryInterface.bulkDelete('Rooms', null, {}),
};
58 changes: 58 additions & 0 deletions src/services/facilityService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Sequelize } from 'sequelize';
import uuid from 'uuid/v4';
import db from '../models';
import stringHelper from '../utils/stringHelper';

/**
* @description facility services
* @class FacilitiesService
*/
export default class FacilityService {
/**
* @param {Object} facilityId
* @param {Object} user
* @param {Object} rating
* @returns {Object} updatedFacility
*/
static async rateFacility(facilityId, user, rating) {
const todayDate = new Date();
const facility = await db.Facilities.findOne({ where: { id: facilityId } });
if (!facility) return stringHelper.facilityNotFound;
const booking = await db.Bookings.findOne({
where: {
bookedBy: user.email,
facilityId,
checkin: {
[Sequelize.Op.lte]: todayDate
}
}
});
if (!booking) return stringHelper.notVisitedFacility;
const ratingUser = await db.Ratings.findOne({ where: { facilityId, userId: user.id } });
if (!ratingUser) {
await db.Ratings.create({
id: uuid(),
userId: user.id,
facilityId,
rating: rating * 1,
});
const sum = await db.Ratings.aggregate('rating', 'sum', { where: { facilityId } });
const average = ((await db.Ratings.aggregate('rating', 'avg', { where: { facilityId }, dataType: 'float' })) * 1).toFixed(2);
const updatedFacility = await facility.update({
totalRating: sum, averageRating: average
}, {
returning: true, plain: true
});
return updatedFacility;
}
await ratingUser.update({ rating: rating * 1 }, { returning: true, plain: true });
const sum = await db.Ratings.aggregate('rating', 'sum', { where: { facilityId } });
const average = ((await db.Ratings.aggregate('rating', 'avg', { where: { facilityId }, dataType: 'float' })) * 1).toFixed(2);
const updatedFacility = await facility.update({
totalRating: sum, averageRating: average
}, {
returning: true, plain: true
});
return updatedFacility;
}
}
10 changes: 9 additions & 1 deletion src/services/localesServices/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,13 @@
"Invalid value": "Invalid value",
"password can not be empty": "password can not be empty",
"email can not be empty": "email can not be empty",
"a comment is required with maximum 200 characters": "a comment is required with maximum 200 characters"
"a comment is required with maximum 200 characters": "a comment is required with maximum 200 characters",
"The firstname is required": "The firstname is required",
"First name should be only characters": "First name should be only characters",
"Last name should be only characters": "Last name should be only characters",
"the rating must be provided": "the rating must be provided",
"the rating can only be an integer number less or equal to 5": "the rating can only be an integer number less or equal to 5",
"facility not found": "facility not found",
"you haven't visited this facility yet": "you haven't visited this facility yet",
"facility rated": "facility rated"
}
7 changes: 6 additions & 1 deletion src/services/localesServices/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,10 @@
"Comment not found": "Commentaire non trouvé",
"You are not authorised to delete this comment": "Vous n'êtes pas autorisé à supprimer ce commentaire",
"a comment is required with maximum 200 characters": "un commentaire est requis avec un maximum de 200 caractères",
"Comment is successfully deleted": "Le commentaire a bien été supprimé"
"Comment is successfully deleted": "Le commentaire a bien été supprimé",
"the rating must be provided": "la cote doit être fournie",
"the rating can only be an integer number less or equal to 5": "la note ne peut être qu'un nombre entier inférieur ou égal à 5",
"facility not found": "installation non trouvée",
"you haven't visited this facility yet":"vous n'avez pas encore visité cette installation",
"facility rated": "évalué par l'établissement"
}
34 changes: 34 additions & 0 deletions src/swagger/facilities.swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,37 @@
* '500':
* description: Server Error.
* */

/**
* @swagger
* /api/v1/facilities/rate/{facilityId}:
* patch:
* security:
* - bearerAuth: []
* tags:
* - Facilities
* name: rate a facilility
* summary: a user can rate a facility they visited
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: token
* in: header
* - name: rating
* in: query
* schema:
* type: string
* - name: facilityId
* in: path
* responses:
* '200':
* description: facility rated
* '404':
* description: facility not found
* '403':
* description: you haven't visited this facility yet
* '400':
* description: the rating can only be an integer number less or equal to 5
* */
Loading

0 comments on commit bda13b1

Please sign in to comment.