diff --git a/README.md b/README.md index e3ee45e..a807d51 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,26 @@ Make company global travel and accommodation easy and convenient for the strong - [Swagger:](https://swagger.io/) - [PassportJS](http://www.passportjs.org) +##### Testing Framework + +- Mocha +- Chai +- Chai-HTTP + +##### Project Management + +- [Pivotal Tracker](https://pivotaltracker.com) + +##### Continuous Integration + +- [Travis CI](https://travis-ci.org) +- [Circle CI](https://circleci.com) +- [Coveralls](https://coveralls.io) + +#### API Docs + +- [API Documentation](https://barefoot-nomad-cyclops-staging.herokuapp.com/api-docs) + --- ### Installation diff --git a/public/docs/swaggerDoc.json b/public/docs/swaggerDoc.json index 286a0ef..e7c2e5c 100644 --- a/public/docs/swaggerDoc.json +++ b/public/docs/swaggerDoc.json @@ -42,6 +42,10 @@ { "name": "Comment", "description": "Comments on trip request" + }, + { + "name": "Accommodation", + "description": "Accommodations for Barefoot Nomad" } ], "schemes": ["http", "https"], @@ -385,13 +389,6 @@ } } }, - "securityDefinitions": { - "api_key": { - "type": "apiKey", - "name": "token", - "in": "header" - } - }, "/api/v1/trips": { "post": { "tags": ["Trip"], @@ -464,14 +461,7 @@ } } }, - "securityDefinitions": { - "api_key": { - "type": "apiKey", - "name": "token", - "in": "header" - } - }, - "/api/v1/trips": { + "/api/v1/comment/trips": { "post": { "tags": ["Comment"], "summary": "Allow users to comment on a trip request", @@ -631,94 +621,209 @@ } } } - } - }, - "definitions": { - "Landing": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "created_on": { - "type": "string", - "format": "date-time" + }, + "/api/v1/accommodation": { + "post": { + "tags": ["Accommodation"], + "summary": "Allow Suppliers to create accommodation", + "description": "", + "operationId": "CreateAnAccommodation", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "type", + "in": "body", + "description": "details of the accommodation", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/CreateAnAccommodation" + } + } + ], + "produces": ["application/json"], + "responses": { + "201": { + "description": "Accomodation that was created", + "status": "success", + "type": "object" + } } }, - "xml": { - "name": "Order" + "get": { + "tags": ["Accommodation"], + "summary": "get All Accommodations", + "description": "", + "operationId": "ReturnsAlAccommodations", + "security": [ + { + "bearerAuth": [] + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Comment deleted successful", + "name": "type" + } + } } }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "first_name": { - "type": "string", - "example": "John" - }, - "last_name": { - "type": "string", - "example": "Doe" - }, - "email": { - "type": "string", - "example": "johndoe@company.com" - }, - "password": { - "type": "string" - }, - "token": { - "type": "integer", - "format": "int64" + "/api/v1/accommodation/:accommodationUuid": { + "get": { + "tags": ["Accommodation"], + "summary": "get A Particular Accommodations", + "description": "", + "operationId": "ReturnsAParticularAccommodations", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "accommodationUuid", + "in": "path", + "description": "get details of the accommodation plus the rooms", + "required": true, + "type": "string" + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "status": "success", + "type": "object" + } } - }, - "xml": { - "name": "User" } }, - "CreateReturnTripRequest": { - "type": "object", - "required": [ - "request_type", - "trip_plan", - "leaving_from", - "return_date", - "travel_date", - "destination" - ], - "properties": { - "request_type": { - "type": "string", - "example": "returnTrip" - }, - "trip_plan": { - "type": "string", - "example": "singleCity" - }, - "leaving_from": { - "type": "uuid", - "example": "95ccd25d-2524-4b95-a441-8e2643c4c075" + "definitions": { + "Landing": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "created_on": { + "type": "string", + "format": "date-time" + } }, - "return_date": { - "type": "date", - "example": "08-22-2018" + "xml": { + "name": "Order" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "email": { + "type": "string", + "example": "johndoe@company.com" + }, + "password": { + "type": "string" + }, + "token": { + "type": "integer", + "format": "int64" + } }, - "travel_date": { - "type": "date", - "example": "09-22-2019" + "xml": { + "name": "User" + } + }, + "CreateReturnTripRequest": { + "type": "object", + "required": [ + "request_type", + "trip_plan", + "leaving_from", + "return_date", + "travel_date", + "destination" + ], + "properties": { + "request_type": { + "type": "string", + "example": "returnTrip" + }, + "trip_plan": { + "type": "string", + "example": "singleCity" + }, + "leaving_from": { + "type": "uuid", + "example": "95ccd25d-2524-4b95-a441-8e2643c4c075" + }, + "return_date": { + "type": "date", + "example": "08-22-2018" + }, + "travel_date": { + "type": "date", + "example": "09-22-2019" + }, + "destination": { + "type": "uuid", + "example": "95ccd25d-2524-4b95-a441-8e2643c4c071" + } }, - "destination": { - "type": "uuid", - "example": "95ccd25d-2524-4b95-a441-8e2643c4c071" + "xml": { + "name": "CreateTripRequest" } }, - "xml": { - "name": "CreateTripRequest" + "CreateAnAccommodation": { + "type": "object", + "required": [ + "name", + "description", + "services", + "image_url", + "amenities" + ], + "properties": { + "name": { + "type": "string", + "example": "Shegson House" + }, + "description": { + "type": "string", + "example": "I want to create this accommodation" + }, + "services": { + "type": "array", + "example": "95ccd25d-2524-4b95-a441-8e2643c4c075" + }, + "amenities": { + "type": "array", + "example": "08-22-2018" + }, + "image_url": { + "type": "date", + "example": "https://res.cloudinary/Blessing.png" + } + }, + "xml": { + "name": "CreateAnAccommodation" + } } } } diff --git a/src/controllers/AccommodationFacilityController.js b/src/controllers/AccommodationFacilityController.js new file mode 100644 index 0000000..60f9a80 --- /dev/null +++ b/src/controllers/AccommodationFacilityController.js @@ -0,0 +1,131 @@ +import AccommodationFacilityRepository from '../repositories/AccommodationFacilityRepository'; +import RoomRepository from '../repositories/RoomRepository'; +import { convertToArray } from '../utils'; +import { sendErrorResponse, sendSuccessResponse } from '../utils/sendResponse'; + +/** + * @description AccommodationFacilityController handles all accommodation logics + */ +class AccommodationFacilityController { + /** + * @description createAccommodation handles the logic to create an accommodation facility + * + * @param {object} req is the request object + * + * @param {object} res is the response object + * + * @param {function} next forwards request to the next middleware in the call stack + * + * @returns {object} it returns a response that is an object + */ + static async createAccommodation(req, res, next) { + try { + const { + name, description, image_url: imageUrl, location, amenities, services, + } = req.body; + + const groupedServices = convertToArray(services); + const groupedAmenities = convertToArray(amenities); + const groupedImageUrl = convertToArray(imageUrl); + + const createAccommodation = { + user_uuid: req.userData.uuid, + name, + description, + location, + image_url: groupedImageUrl, + amenities: groupedAmenities, + services: groupedServices, + }; + + const createdAccommodation = await AccommodationFacilityRepository + .create(createAccommodation); + + const roomDetails = { + accommodation_uuid: createdAccommodation.uuid, + name: req.body.room_name, + type: req.body.room_type, + cost: req.body.cost + }; + + const createdRoom = (roomDetails.name) ? await RoomRepository.create(roomDetails) : undefined; + + return sendSuccessResponse(res, 201, { ...createdAccommodation, room: createdRoom }); + } catch (err) { + return next(err); + } + } + + /** + * @param {Object} req - HTTP request object + * + * @param {Object} res - HTTP response object + * + * @param {Function} next - Function to trigger next middleware + * + * @return {Object} Object resoponse with current user created status + */ + static async getAllAccommodation(req, res, next) { + try { + const userTrips = await AccommodationFacilityRepository.getAll(); + return sendSuccessResponse(res, 200, userTrips); + } catch (err) { + return next(err); + } + } + + /** + * @description getOneAccommodation method gets all created accommodations + * + * @param {Object} req - HTTP request object + * + * @param {Object} res - HTTP response object + * + * @param {Function} next - Function to trigger next middleware + * + * @return {Object} Object resoponse with current user created status + */ + static async getOneAccommodation(req, res, next) { + try { + const { accommodationUuid } = req.params; + const accommodation = await AccommodationFacilityRepository + .getById({ uuid: accommodationUuid }); + + if (!accommodation) return sendErrorResponse(res, 404, 'This accommodation facility does not exist'); + + const accommodationFacility = await AccommodationFacilityRepository + .getOne({ uuid: accommodationUuid }, ['rooms']); + return sendSuccessResponse(res, 200, accommodationFacility); + } catch (err) { + return next(err); + } + } + + /** + * @description createRoom is a method that handles the logic to create a room + * + * @param {object} req is the request object + * + * @param {object} res is the response object + * + * @param {function} next forwards request to the next middleware in the czall stack + * + * @returns {object} it returns a response that is an object + */ + static async createRoom(req, res, next) { + try { + const { accommodation_uuid: accommmodationUuid } = req.body; + const accommodation = await AccommodationFacilityRepository + .getById({ uuid: accommmodationUuid }); + + if (!accommodation) return sendErrorResponse(res, 404, 'This accommodation facility does not exist'); + + const createdRoom = await RoomRepository.create(req.body); + return sendSuccessResponse(res, 201, createdRoom); + } catch (err) { + return next(err); + } + } +} + +export default AccommodationFacilityController; diff --git a/src/database/migrations/20190827166545-create-managers.js b/src/database/migrations/20190827166545-create-managers.js index deb0379..331fd6d 100644 --- a/src/database/migrations/20190827166545-create-managers.js +++ b/src/database/migrations/20190827166545-create-managers.js @@ -7,6 +7,7 @@ export default { defaultValue: Sequelize.UUIDV4 }, user_uuid: { + allowNull: false, type: Sequelize.UUID, onDelete: 'CASCADE', references: { diff --git a/src/database/migrations/20190911100155-create-accommodation-facilty.js b/src/database/migrations/20190911100155-create-accommodation-facilty.js new file mode 100644 index 0000000..e767c80 --- /dev/null +++ b/src/database/migrations/20190911100155-create-accommodation-facilty.js @@ -0,0 +1,46 @@ +export default { + up: (queryInterface, Sequelize) => queryInterface.createTable('AccommodationFacilities', { + uuid: { + allowNull: false, + primaryKey: true, + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4 + }, + user_uuid: { + allowNull: false, + type: Sequelize.UUID, + onDelete: 'CASCADE', + references: { + model: 'Users', + key: 'uuid' + } + }, + name: { + type: Sequelize.STRING + }, + description: { + type: Sequelize.STRING + }, + image_url: { + type: Sequelize.ARRAY(Sequelize.TEXT) + }, + services: { + type: Sequelize.ARRAY(Sequelize.TEXT) + }, + amenities: { + type: Sequelize.ARRAY(Sequelize.TEXT) + }, + location: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: (queryInterface) => queryInterface.dropTable('AccommodationFacilities') +}; diff --git a/src/database/migrations/20190912074341-create-room.js b/src/database/migrations/20190912074341-create-room.js new file mode 100644 index 0000000..02a0bfc --- /dev/null +++ b/src/database/migrations/20190912074341-create-room.js @@ -0,0 +1,40 @@ +export default { + up: (queryInterface, Sequelize) => queryInterface.createTable('Rooms', { + uuid: { + allowNull: false, + primaryKey: true, + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4 + }, + accommodation_uuid: { + allowNull: false, + type: Sequelize.UUID, + onDelete: 'CASCADE', + references: { + model: 'AccommodationFacilities', + key: 'uuid' + } + }, + name: { + allowNull: false, + type: Sequelize.STRING + }, + type: { + allowNull: false, + type: Sequelize.STRING + }, + cost: { + allowNull: false, + type: Sequelize.FLOAT(11) + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: queryInterface => queryInterface.dropTable('Rooms') +}; diff --git a/src/database/seeders/20190825080303-create-users.js b/src/database/seeders/20190825080303-create-users.js index 42e60fd..c4ec88a 100644 --- a/src/database/seeders/20190825080303-create-users.js +++ b/src/database/seeders/20190825080303-create-users.js @@ -6,39 +6,39 @@ export default { up: (queryInterface, Sequelize) => { const UsersData = [ { - uuid: uuid(), + uuid: '95ccd25d-2524-4b95-a441-8e2643c4c072', name: 'Efe Justin', email: 'efejustin3@gmail.com', + password: hashPassword('Jei12345', 10), role: 'Manager', - password: hashPassword('Jei12345'), is_verified: true, created_at: Sequelize.literal('NOW()'), updated_at: Sequelize.literal('NOW()') - }, + }, { - uuid: uuid(), - name: 'Efe Just', + uuid: '95ccd25d-2524-4b95-a441-8e2643c4c073', + name: 'Efe Justins', email: 'efejustin@gmail.com', + password: hashPassword('Jei12345', 10), role: 'Requester', - password: hashPassword('John3162'), is_verified: true, created_at: Sequelize.literal('NOW()'), updated_at: Sequelize.literal('NOW()') - }, + }, { uuid: uuid(), name: 'Makaraba Blessing', email: 'blessingpeople@gmail.com', role: 'Manager', is_verified: false, - password: hashPassword('Bloated36'), + password: hashPassword('Bloated36', 10), created_at: Sequelize.literal('NOW()'), updated_at: Sequelize.literal('NOW()') }, { uuid: 'abef6009-48be-4b38-80d0-b38c1bc39922', email: 'greatness@andela.com', - password: hashPassword('Password123'), + password: hashPassword('Password123', 10), name: 'Albert Faith', is_verified: true, created_at: Sequelize.literal('NOW()'), @@ -47,7 +47,7 @@ export default { { uuid: '407d0d03-be0d-477c-badd-5df63b04307e', email: 'mymail@naija.com', - password: hashPassword('Password123'), + password: hashPassword('Password123', 10), name: 'Robert Dick', is_verified: true, created_at: Sequelize.literal('NOW()'), @@ -65,11 +65,11 @@ export default { }, { uuid: '95ccd25d-2524-4b95-a441-8e2643c4c079', - email: faker.internet.email(), + email: 'somemail@yahoo.com', password: hashPassword('Password123', 10), - name: faker.name.findName(), - role: 'Manager', is_verified: false, + role: 'Manager', + name: faker.name.findName(), created_at: Sequelize.literal('NOW()'), updated_at: Sequelize.literal('NOW()') }, @@ -104,7 +104,17 @@ export default { image_url: 'http://images.com/myimagefile', created_at: new Date(), updated_at: new Date() - } + }, + { + uuid: 'fd847314-71c5-4385-95ee-966c975a3ddd', + name: 'Suspie Abobo', + email: 'suspieabobo@yahoo.com', + role: 'Supplier', + is_verified: true, + password: hashPassword('Bloated36', 10), + created_at: Sequelize.literal('NOW()'), + updated_at: Sequelize.literal('NOW()') + }, ]; return queryInterface.bulkInsert('Users', UsersData, {}); }, diff --git a/src/database/seeders/20190902194109-roles.js b/src/database/seeders/20190902194109-roles.js index 2420564..1bb49ee 100644 --- a/src/database/seeders/20190902194109-roles.js +++ b/src/database/seeders/20190902194109-roles.js @@ -31,7 +31,13 @@ export default { name: 'Requester', createdAt: Sequelize.literal('NOW()'), updatedAt: Sequelize.literal('NOW()') - } + }, + { + uuid: uuid.v4(), + name: 'Supplier', + createdAt: Sequelize.literal('NOW()'), + updatedAt: Sequelize.literal('NOW()') + }, ], {}), down: queryInterface => queryInterface.bulkDelete('Roles', null, {}) diff --git a/src/middlewares/accommodationValidation.js b/src/middlewares/accommodationValidation.js new file mode 100644 index 0000000..06ff051 --- /dev/null +++ b/src/middlewares/accommodationValidation.js @@ -0,0 +1,77 @@ +import { + validate, isRequired, inValidImageType, inValidCostType, +} from '../modules/validator'; +import { sendErrorResponse } from '../utils/sendResponse'; + +/** + * @description AccommodationFacilityValidator is clas that handles user data validation + */ +export default class AccommodationFacilityValidator { + /** + * + * @param {req} req object + * + * @param {res} res object + * + * @param {next} next forwards request to the next middleware function + * + * @returns {obj} reurns an response object + */ + static createAccommodation(req, res, next) { + const { + name, description, image_url: imageUrl, location, services, amenities, + room_name: roomName, room_type: roomType, cost + } = req.body; + + let addedSchema; + const schema = { + name: isRequired(name), + description: isRequired(description), + image_url: inValidImageType(imageUrl), + location: isRequired(location), + amenities: isRequired(amenities), + services: isRequired(services), + }; + + if (roomName || roomType || cost) { + addedSchema = { + room_name: isRequired(roomName), + room_type: isRequired(roomType), + cost: inValidCostType(cost) + }; + } + + const updatedSchema = { ...schema, ...addedSchema }; + + const error = validate(updatedSchema); + if (error) return sendErrorResponse(res, 422, error); + return next(); + } + + /** + * + * @param {req} req object + * + * @param {res} res object + * + * @param {next} next forwards request to the next middleware function + * + * @returns {obj} reurns an response object + */ + static createRoom(req, res, next) { + const { + accommodation_uuid: accommodationUuid, name, type, cost, + } = req.body; + + const schema = { + accommodation_uuid: isRequired(accommodationUuid), + name: isRequired(name), + type: isRequired(type), + cost: inValidCostType(cost) + }; + + const error = validate(schema); + if (error) return sendErrorResponse(res, 422, error); + return next(); + } +} diff --git a/src/middlewares/requestValidation.js b/src/middlewares/requestValidation.js index 2cce56b..119e49f 100644 --- a/src/middlewares/requestValidation.js +++ b/src/middlewares/requestValidation.js @@ -4,7 +4,7 @@ import { import { sendErrorResponse } from '../utils/sendResponse'; /** - * @description userAuth is clas that handles user data validation + * @description requestValidator is clas that handles user data validation */ export default class requestValidator { /** diff --git a/src/middlewares/verifyRoles.js b/src/middlewares/verifyRoles.js index 4373f61..91906c0 100644 --- a/src/middlewares/verifyRoles.js +++ b/src/middlewares/verifyRoles.js @@ -46,6 +46,24 @@ class VerifyRoles { } next(); } + + /** + * @description verify and authorize Requester roles + * + * @param {*} req + * + * @param {*} res + * + * @param {*} next + * + * @returns {*} pass control to the next middleware + */ + async verifySupplier(req, res, next) { + if (req.userData.role !== 'Supplier') { + return sendErrorResponse(res, 401, 'Unauthorized access'); + } + next(); + } } export default new VerifyRoles(); diff --git a/src/models/AccommodationFacilty.js b/src/models/AccommodationFacilty.js new file mode 100644 index 0000000..40ad8eb --- /dev/null +++ b/src/models/AccommodationFacilty.js @@ -0,0 +1,51 @@ + +export default (sequelize, DataTypes) => { + const AccommodationFacility = sequelize.define('AccommodationFacility', { + uuid: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4 + }, + user_uuid: { + allowNull: false, + type: DataTypes.UUID, + onDelete: 'CASCADE', + references: { + model: 'Users', + key: 'uuid' + } + }, + name: { + type: DataTypes.STRING + }, + description: { + type: DataTypes.STRING + }, + image_url: { + type: DataTypes.ARRAY(DataTypes.TEXT) + }, + services: { + type: DataTypes.ARRAY(DataTypes.TEXT) + }, + amenities: { + type: DataTypes.ARRAY(DataTypes.TEXT) + }, + location: { + type: DataTypes.STRING + }, + }, {}); + AccommodationFacility.associate = (models) => { + AccommodationFacility.belongsTo(models.User, { + as: 'user', + foreignKey: 'user_uuid', + onDelete: 'CASCADE' + }); + AccommodationFacility.hasMany(models.Room, { + as: 'rooms', + foreignKey: 'accommodation_uuid', + onDelete: 'CASCADE' + }); + }; + return AccommodationFacility; +}; diff --git a/src/models/Room.js b/src/models/Room.js new file mode 100644 index 0000000..a828814 --- /dev/null +++ b/src/models/Room.js @@ -0,0 +1,25 @@ +export default (sequelize, DataTypes) => { + const Room = sequelize.define('Room', { + uuid: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4 + }, + name: { + allowNull: false, + type: DataTypes.STRING + }, + type: { + allowNull: false, + type: DataTypes.STRING + }, + cost: { + allowNull: false, + type: DataTypes.STRING + }, + }, {}); + Room.associate = () => { + }; + return Room; +}; diff --git a/src/modules/validator.js b/src/modules/validator.js index 8906916..8b67aff 100644 --- a/src/modules/validator.js +++ b/src/modules/validator.js @@ -80,7 +80,7 @@ export const inValidPassword = password => { * @returns {string} string is the type of data the function returns */ -export const inValidDate = (date) => { +export const inValidDate = date => { if (!date) return undefined; const decision = moment(date, 'MM/DD/YYYY', true).isValid(); if (!decision) return 'date should be of the form MM/DD/YYYY'; @@ -105,7 +105,7 @@ export const inValidReturnType = (condition, payload) => { return false; }; -export const inValidLocationId = (locationId) => { +export const inValidLocationId = locationId => { if (!locationId) return undefined; if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(locationId)) { return 'Your departure/destination should contain the uuid of the office location'; @@ -113,6 +113,19 @@ export const inValidLocationId = (locationId) => { return false; }; +export const inValidImageType = imageUrl => { + if (!imageUrl) return undefined; + if (!/(\.jpg|\.png|\.jpeg)$/.test(imageUrl)) return 'only jpeg, jpg, and png image formats are accepted'; + return false; +}; + +export const inValidCostType = cost => { + if (!cost) return undefined; + if (!/^\d{1,11}(\.\d{1,2})?$/.test(cost)) return 'cost should be in the form 565648, 3676372.89,'; + return false; +}; + + export const validate = obj => { const result = {}; Object.keys(obj).forEach((key) => { diff --git a/src/repositories/AccommodationFacilityRepository.js b/src/repositories/AccommodationFacilityRepository.js new file mode 100644 index 0000000..6945fda --- /dev/null +++ b/src/repositories/AccommodationFacilityRepository.js @@ -0,0 +1,99 @@ +/* eslint-disable camelcase */ +/** + * @fileoverview Contains the AccommodationRepository class, an interface for querying User table + * + * @author TeamCyclops + * + * @requires models/Accommodation.js + */ + +import Models from '../models'; + +const { AccommodationFacility } = Models; + +/** + * @description AccommodationFacilityRepository handles our methods to query our accommodation table + * + * @class AccommodationFacilityRepostiory + */ +class AccommodationFacilityRepository { + /** + * Trip Model constructor + * + * @description constructor handles the properties/univsersal data for our requestRepository + * + * @constructor + * + */ + constructor() { + this.db = AccommodationFacility; + } + + /** + * @description create handles method that query our database + * + * @param {object} accommodationDetails refers to the details of the user's accommodation + * + * @returns {object} the details of the accommodation that was created + */ + async create(accommodationDetails) { + try { + const { dataValues } = await this.db.create(accommodationDetails); + return dataValues; + } catch (err) { + throw new Error(err); + } + } + + + /** + * @description getById is a function that search for an accommodation + * + * @param {object} condition limits the search of the accommodation + * + * @returns {object} the details of the accommodation that has been searched for + */ + async getById(condition) { + try { + const accommodationDetails = await this.db.findOne({ where: condition }); + return accommodationDetails; + } catch (err) { + throw new Error(err); + } + } + + /** + * @description Returns user's trip request information based on the provided parameters + * + * @param {Object} condition checks trip request required parameter for a user + * + * @return {Object} returns trip request details + */ + async getAll() { + try { + const accommodations = this.db.findAll(); + return accommodations; + } catch (err) { + throw new Error(err); + } + } + + /** + * @description getOne methods returns accommodation details + * + * @param {Object} condition checks required accommodation parameter + * + * @param {Object} include adds users managers + * + * @return {Object} returns accommodation details with all rooms details + */ + async getOne(condition = {}, include = '') { + try { + return await this.db.findOne({ where: condition, include }); + } catch (e) { + throw new Error(e); + } + } +} + +export default new AccommodationFacilityRepository(); diff --git a/src/repositories/RoomRepository.js b/src/repositories/RoomRepository.js new file mode 100644 index 0000000..e7daf45 --- /dev/null +++ b/src/repositories/RoomRepository.js @@ -0,0 +1,49 @@ +/* eslint-disable camelcase */ +/** + * @fileoverview Contains the User Auth Repository class, an interface for querying User table + * + * @author TeamCyclops + * + * @requires models/Room.js + */ + +import Models from '../models'; + +const { Room } = Models; + +/** + * @description RoomRepository handles our methods to query our accommodation table + * + * @class RoomRepostiory + */ +class RoomRepository { + /** + * Trip Model constructor + * + * @description constructor handles the properties/univsersal data for our requestRepository + * + * @constructor + * + */ + constructor() { + this.db = Room; + } + + /** + * @description create handles method that query our database + * + * @param {object} roomDetails refers to the details of the room + * + * @returns {object} the details of the room that was created for a particular accommodation + */ + async create(roomDetails) { + try { + const { dataValues } = await this.db.create(roomDetails); + return dataValues; + } catch (err) { + throw new Error(err); + } + } +} + +export default new RoomRepository(); diff --git a/src/repositories/UserRepository.js b/src/repositories/UserRepository.js index 22cd583..3d637e2 100644 --- a/src/repositories/UserRepository.js +++ b/src/repositories/UserRepository.js @@ -213,9 +213,10 @@ class UserRepository { { role_uuid: uuid, role: newRole }, { where: { email }, returning: true, plain: true } ); + const { uuid: userUuid } = data[1].dataValues; if (newRole === 'Manager') { await Manager.create( - { uuid: data.uuid } + { user_uuid: userUuid } ); } diff --git a/src/routes/accommodation.js b/src/routes/accommodation.js new file mode 100644 index 0000000..d9c565c --- /dev/null +++ b/src/routes/accommodation.js @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import authenticateUser from '../middlewares/authenticateUser'; +import AccommodationFacilityController from '../controllers/AccommodationFacilityController'; +import verifyRoles from '../middlewares/verifyRoles'; +import AccommodationFacilityValidator from '../middlewares/accommodationValidation'; + +const router = Router(); + +router.post('/accommodation', + [authenticateUser, verifyRoles.verifySupplier], + AccommodationFacilityValidator.createAccommodation, + AccommodationFacilityController.createAccommodation); +router.get('/accommodation', authenticateUser, AccommodationFacilityController.getAllAccommodation); +router.get('/accommodation/:accommodationUuid', + authenticateUser, + AccommodationFacilityController.getOneAccommodation); + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js index 342407b..9b19e7b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,6 +6,8 @@ import swaggerDoc from '../../public/docs/swaggerDoc.json'; import user from './user'; import trip from './trip'; import office from './office'; +import accommodation from './accommodation'; +import room from './room'; import notifications from './notifications'; import comment from './comment'; @@ -15,7 +17,7 @@ export default (app) => { data: 'Welcome to the Cyclops Barefoot Nomad backend API' })); - app.use('/api/v1', [user, trip, office, comment]); + app.use('/api/v1', [user, trip, office, comment, accommodation, room]); // Add notification endpoints to application app.use('/api/v1/notifications', notifications); diff --git a/src/routes/room.js b/src/routes/room.js new file mode 100644 index 0000000..1ce31f4 --- /dev/null +++ b/src/routes/room.js @@ -0,0 +1,14 @@ +import { Router } from 'express'; + +import authenticateUser from '../middlewares/authenticateUser'; +import AccommodationFacilityController from '../controllers/AccommodationFacilityController'; +import AccommodationFacilityValidator from '../middlewares/accommodationValidation'; + +const router = Router(); + +router.post('/room', + authenticateUser, + AccommodationFacilityValidator.createRoom, + AccommodationFacilityController.createRoom); + +export default router; diff --git a/src/utils/index.js b/src/utils/index.js index 638a827..323405e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -9,6 +9,9 @@ export const getDay = (date) => { return Math.floor(dateInMillisec / 86400000); }; + +export const convertToArray = arrayItems => arrayItems.replace(/\s/g, '').split(','); + const { BlackListedToken } = models; /** diff --git a/tests/accommodation.test.js b/tests/accommodation.test.js new file mode 100644 index 0000000..8fad109 --- /dev/null +++ b/tests/accommodation.test.js @@ -0,0 +1,243 @@ +import { describe, it } from 'mocha'; +import chai, { expect } from 'chai'; +import chaiHttp from 'chai-http'; +import { createToken } from '../src/modules/tokenProcessor'; +import app from '../src'; + +chai.use(chaiHttp); + +const seededUser = { + uuid: 'fd847314-71c5-4385-95ee-966c975a3ddd', + name: 'Suspie Abobo', + email: 'suspieabobo@yahoo.com', + role: 'Supplier', +}; + +const token = createToken(seededUser); + +let accommodationUuid; +const fakeAccommodationUuid = 'fd847314-71c5-4385-95ee-966c975a3ddd'; +describe('ACCOMMODATION TESTS', () => { + it('Should create an accommodation', (done) => { + chai.request(app) + .post('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + name: 'Eko Lodge', + description: 'We provide you all', + services: 'laundry, repairs', + amenities: 'AC, Internet, Electricity', + image_url: 'http://res.cloudinary.com/blessing.png', + location: '9, Omoru Street' + }) + .end((err, res) => { + expect(res.status).eql(201); + expect(res.body.status).to.eql('success'); + expect(res.body.data).to.have.all.keys('uuid', 'user_uuid', 'name', 'description', 'location', 'services', 'amenities', 'image_url', 'createdAt', 'updatedAt'); + done(); + }); + }); + + it('Should create an accommodation with a room', (done) => { + chai.request(app) + .post('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + name: 'Eko Lodge', + description: 'We provide you all', + services: 'laundry, repairs', + amenities: 'AC, Internet, Electricity', + image_url: 'http://res.cloudinary.com/blessing.png', + location: '9, Omoru Street', + room_name: 'Eko 1', + room_type: 'One Bedroom', + cost: '3000000' + }) + .end((err, res) => { + expect(res.status).eql(201); + expect(res.body.status).to.eql('success'); + expect(res.body.data).to.have.all.keys( + 'uuid', 'user_uuid', 'name', 'description', + 'location', 'services', 'amenities', + 'image_url', 'room', 'createdAt', 'updatedAt' + ); + done(); + }); + }); + + it('Should return an error upon empty inputs', (done) => { + chai.request(app) + .post('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + name: '', + description: '', + services: '', + amenities: '', + image_url: '', + location: '', + room_name: '', + room_type: '3 Bedroom', + cost: '' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.have.all.keys( + 'name', 'description', 'services', + 'amenities', 'location', 'image_url', + 'room_name', 'cost' + ); + done(); + }); + }); + + it('Should return an error upon invalid cost', (done) => { + chai.request(app) + .post('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + name: 'Eko Lodge', + description: 'We provide you all', + services: 'laundry, repairs', + amenities: 'AC, Internet, Electricity', + image_url: 'http://res.cloudinary.com/blessing.png', + location: '9, Omoru Street', + room_name: 'Eko 1', + room_type: 'One Bedroom', + cost: '300000.22.33' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.have.key({ cost: 'cost should be in the form 565648, 3676372.89,' }); + done(); + }); + }); + + it('Should return an error upon invalid image type', (done) => { + chai.request(app) + .post('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + name: 'Eko Lodge', + description: 'We provide you all', + services: 'laundry, repairs', + amenities: 'AC, Internet, Electricity', + image_url: 'http://res.cloudinary.com/blessing', + location: '9, Omoru Street', + room_name: 'Eko 1', + room_type: 'One Bedroom', + cost: '300000.22' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.have.key({ image_url: 'only jpeg, jpg, and png image formats are accepted,' }); + done(); + }); + }); + + it('Should return all accommodations', (done) => { + chai.request(app) + .get('/api/v1/accommodation') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .end((err, res) => { + expect(res.status).eql(200); + expect(res.body.status).to.eql('success'); + expect(res.body.data).to.be.an('array'); + const [accommodationDetails] = res.body.data; + accommodationUuid = accommodationDetails.uuid; + done(); + }); + }); + + it('Should return a specific accommodation', (done) => { + chai.request(app) + .get(`/api/v1/accommodation/${accommodationUuid}`) + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .end((err, res) => { + expect(res.status).eql(200); + expect(res.body.status).to.eql('success'); + expect(res.body.data).to.be.an('object'); + done(); + }); + }); + + it('Should return an error if accommodation does not exist', (done) => { + chai.request(app) + .get(`/api/v1/accommodation/${fakeAccommodationUuid}`) + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .end((err, res) => { + expect(res.status).eql(404); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.be.eql('This accommodation facility does not exist'); + done(); + }); + }); + + it('Should create an room for a particular accommodation facility', (done) => { + chai.request(app) + .post('/api/v1/room') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + accommodation_uuid: accommodationUuid, + name: '', + type: 'One Bedroom', + cost: '300000.22' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.be.have.key({ name: 'name is required' }); + done(); + }); + }); + + it('Should create an room for a particular accommodation facility', (done) => { + chai.request(app) + .post('/api/v1/room') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + accommodation_uuid: accommodationUuid, + name: 'Better 3', + type: '', + cost: '300000.22' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.be.have.key({ type: 'name is required' }); + done(); + }); + }); + + it('Should create an room for a particular accommodation facility', (done) => { + chai.request(app) + .post('/api/v1/room') + .set('Content-Type', 'Application/json') + .set('authorization', `Bearer ${token}`) + .send({ + accommodation_uuid: accommodationUuid, + name: 'Better 4', + type: 'One Bedroom', + cost: '' + }) + .end((err, res) => { + expect(res.status).eql(422); + expect(res.body.status).to.eql('error'); + expect(res.body.error).to.be.have.key({ cost: 'cost is required' }); + done(); + }); + }); +}); diff --git a/tests/comment.test.js b/tests/comment.test.js index 8f1a281..d1557c9 100644 --- a/tests/comment.test.js +++ b/tests/comment.test.js @@ -21,8 +21,8 @@ const seededUserI = { }; const seededUserII = { - uuid: '95ccd25d-2524-4b95-a441-8e2643c4c072', - name: 'Efe Just', + uuid: '95ccd25d-2524-4b95-a441-8e2643c4c073', + name: 'Efe Justins', email: 'efejustin@gmail.com', role: 'Requester', }; diff --git a/tests/index.js b/tests/index.js index 77b8857..86d0003 100644 --- a/tests/index.js +++ b/tests/index.js @@ -13,6 +13,9 @@ import './user.profile.view.test'; import './social.login.test'; import './userRole.test'; +// Accommodation tests +import './accommodation.test'; + // Request tests import './request.test';