diff --git a/src/controller/ExchangeController.ts b/src/controller/ExchangeController.ts index da43755..2354aa6 100644 --- a/src/controller/ExchangeController.ts +++ b/src/controller/ExchangeController.ts @@ -1,33 +1,39 @@ import { NextFunction, Request, Response, Router } from 'express'; import { check, ValidationError, validationResult } from 'express-validator'; -import { exchangeStatus } from '../entity/Exchange'; +import { ExchangeStatus } from '../entity/Exchange'; import { ExchangeService } from '../services/ExchangeService'; import CallBack from '../services/FunctionStatusCode'; import Logger from '../services/Logger'; +import { UserService } from '../services/UserService'; export class ExchangeController { private exchangeService: ExchangeService; + private userService: UserService; public router: Router; constructor() { this.exchangeService = new ExchangeService(); + this.userService = new UserService(); this.router = Router(); this.routes(); } public routes() { - this.router.get('/:id', this.getOne); this.router.get('/', this.getExchanges); + this.router.get('/:id', this.getOne); + this.router.get('/:id/validate', this.validateExchange) + this.router.get('/:id/cancel', this.cancelExchange) this.router.post( '/', [ check('suggesterStudent') .exists().withMessage('Field "suggesterStudent" is missing') - .isNumeric().trim().escape(), + .isUUID().trim().escape(), check('aimStudent') .exists().withMessage('Field "aimStudent" is missing') - .isNumeric().trim().escape(), + .isUUID().custom((value, { req }) => value !== req.body.suggesterStudent).withMessage('Field "aimStudent" must be different from "suggesterStudent"') + .trim().escape(), check('exchangedTimeslot') .exists().withMessage('Field "exchangedTimeslot" is missing') .isNumeric().trim().escape(), @@ -37,19 +43,21 @@ export class ExchangeController { check('status') .exists().withMessage('Field "status" is missing'). custom((value: String) => { - return (Object.values(exchangeStatus) as String[]).includes(value); + return (Object.values(ExchangeStatus) as String[]).includes(value); }).trim().escape(), ], this.postOne); this.router.put( '/:id', [ - check('suggesterStudent').isNumeric().trim().escape(), - check('aimStudent').isNumeric().trim().escape(), + check('suggesterStudent').isUUID().trim().escape(), + check('aimStudent') + .isUUID().custom((value, { req }) => value !== req.body.suggesterStudent).withMessage('Field "aimStudent" must be different from "suggesterStudent"') + .trim().escape(), check('exchangedTimeslot').isNumeric().trim().escape(), check('desiredTimeslot').isNumeric().trim().escape(), check('status').custom((value: String) => { - return (Object.values(exchangeStatus) as String[]).includes(value); + return (Object.values(ExchangeStatus) as String[]).includes(value); }).trim().escape(), ], this.putOne); @@ -72,18 +80,136 @@ export class ExchangeController { * GET exchange by id * @param req Express Request * @param res Express Response + * @param next Express NextFunction + */ + public getOne = async (req: Request, res: Response, next: NextFunction) => { + const exchangeId = req.params.id; + if (exchangeId === undefined || exchangeId === null) { + res.status(400).send("Error, parameter id is missing or wrong").end(); + return; + } else { + res.send(await this.exchangeService.findById(parseInt(exchangeId, 10))) + return; + } + } + + /** + * Validate the exchange + * @param req Express Request + * @param res Express Response * @param next Express NextFunction * @returns */ - public getOne = async (req: Request, res: Response, next: NextFunction) => { + public validateExchange = async (req: Request, res: Response, next: NextFunction) => { const exchangeId = req.params.id; - if (typeof exchangeId === undefined || exchangeId === null) { - res.status(400).send("Error, parameter id is missing or wrong"); + if (exchangeId === undefined || exchangeId === null) { + res.status(400).send("Error, parameter id is missing or wrong").end(); return; + } else { + // Get the exchange to validate + const exchange = await this.exchangeService.findById(parseInt(exchangeId, 10)); + + if (exchange !== undefined) { + // Verify that the user who validate is the right one + const user = await this.userService.findById(res.locals.user.id); + if (user === undefined) { + Logger.error("User no found => id: " + res.locals.user.id); + res.status(403).send("Unauthorized").end(); + return; + } else { + // Body validation is now complete + const responseCode = await this.exchangeService.validate(exchange, user); + + switch (responseCode) { + case CallBack.Status.DB_ERROR: { + res.status(404) + .send("An error occurred while validating the exchange. Please try later and verify values sent").end(); + break; + } + case CallBack.Status.LOGIC_ERROR: { + res.status(401) + .send("Unauthorized to validate this exchange") + .end(); + break; + } + case CallBack.Status.FAILURE: { + res.status(404) + .send("Fail to validate the exchange. Please try later or verify values sent") + .end(); + break; + } + case CallBack.Status.SUCCESS: { + res.end(); + break; + } + default: + res.end(); + break; + } + } + } else { + Logger.error("Exchange not found => id: " + exchangeId); + res.status(404).send("Entity not found").end(); + return; + } } - else{ - res.send(await this.exchangeService.findUser(parseInt(exchangeId, 10))) + } + + /** + * Cancel the exchange + * @param req Express Request + * @param res Express Response + * @param next Express NextFunction + * @returns + */ + public cancelExchange = async (req: Request, res: Response, next: NextFunction) => { + const exchangeId = req.params.id; + if (exchangeId === undefined || exchangeId === null) { + res.status(400).send("Error, parameter id is missing or wrong").end(); return; + } else { + // Get the exchange to validate + const exchange = await this.exchangeService.findById(parseInt(exchangeId, 10)); + + if (exchange !== undefined) { + // Verify that the user who validate is the right one + const user = await this.userService.findById(res.locals.user.id); + if (user === undefined) { + Logger.error("User no found => id: " + res.locals.user.id); + res.status(403).send("Unauthorized").end(); + } else { + // Body validation is now complete + const responseCode = await this.exchangeService.cancel(exchange, user); + switch (responseCode) { + case CallBack.Status.DB_ERROR: { + res.status(404) + .send("An error occurred while validating the exchange. Please try later and verify values sent").end(); + break; + } + case CallBack.Status.LOGIC_ERROR: { + res.status(401) + .send("Unauthorized to validate this exchange") + .end(); + break; + } + case CallBack.Status.FAILURE: { + res.status(404) + .send("Fail to validate the exchange. Please try later or verify values sent") + .end(); + break; + } + case CallBack.Status.SUCCESS: { + res.end(); + break; + } + default: break; + } + } + } else { + Logger.error("Exchange not found => id: " + exchangeId); + res.status(404).send("Entity not found").end(); + return; + } } } diff --git a/src/entity/Exchange.ts b/src/entity/Exchange.ts index 344992a..18b28bd 100644 --- a/src/entity/Exchange.ts +++ b/src/entity/Exchange.ts @@ -1,11 +1,15 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from "typeorm"; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique, UpdateDateColumn } from "typeorm"; import { TimeSlot } from "./TimeSlot"; import { User } from "./User"; -export enum exchangeStatus { +export enum ExchangeStatus { 'PENDING' = 'PENDING', - 'ACCEPTED' ='ACCEPTED', - 'VALIDATED' ='VALIDATED' + 'SUGGESTER_STUDENT_ACCEPTED' = 'SUGGESTER_STUDENT_ACCEPTED', + 'AIM_STUDENT_VALIDATED' = 'AIM_STUDENT_VALIDATED', + 'VALIDATED' = 'VALIDATED', + 'SUGGESTER_STUDENT_CANCELLED' = 'SUGGESTER_STUDENT_CANCELLED', + 'AIM_STUDENT_CANCELLED' = 'AIM_STUDENT_CANCELLED', + 'CANCELLED' = 'CANCELLED' }; /** @@ -17,26 +21,30 @@ export enum exchangeStatus { export class Exchange { @PrimaryGeneratedColumn() - id?: number; + id!: number; @Column({ type: "enum", - enum: ["PENDING" , "ACCEPTED" , "VALIDATED"], + enum: ["PENDING" , "SUGGESTER_STUDENT_ACCEPTED", "AIM_STUDENT_VALIDATED", "VALIDATED" , "SUGGESTER_STUDENT_CANCELLED", + "AIM_STUDENT_CANCELLED", "CANCELLED"], default: "PENDING" }) - status?: String = exchangeStatus.PENDING; + status: String = ExchangeStatus.PENDING; + + @UpdateDateColumn() + readonly updatedAt!: Date; @ManyToOne(() => TimeSlot, timeSlot => timeSlot.id) - exchangedTimeslot?: TimeSlot; + exchangedTimeslot!: TimeSlot; @ManyToOne(() => TimeSlot, timeSlot => timeSlot.id) - desiredTimeslot?: TimeSlot; + desiredTimeslot!: TimeSlot; @ManyToOne(() => User, user => user.id) - suggesterStudent?: User; + suggesterStudent!: User; @ManyToOne(() => User, user => user.id) - aimStudent?: User; + aimStudent!: User; /** * Constructor for Exchange diff --git a/src/repository/ExchangeRepository.ts b/src/repository/ExchangeRepository.ts index d223cf2..1825bf2 100644 --- a/src/repository/ExchangeRepository.ts +++ b/src/repository/ExchangeRepository.ts @@ -1,17 +1,18 @@ import { EntityRepository, Repository } from "typeorm"; -import { Exchange, exchangeStatus } from "../entity/Exchange"; +import { Exchange, ExchangeStatus } from "../entity/Exchange"; +import { User } from "../entity/User"; @EntityRepository(Exchange) export class ExchangeRepository extends Repository { public findOneWithRelations = (id: number) => { return this.createQueryBuilder('exchange') - .where("exchange.id = :id", { id }) + .where("exchange.id = :id", { id }) .innerJoinAndSelect('exchange.exchangedTimeslot', 'exchangedTimeslot') .innerJoinAndSelect('exchange.desiredTimeslot', 'desiredTimeslot') .innerJoinAndSelect('exchange.suggesterStudent', 'suggesterStudent') .innerJoinAndSelect('exchange.aimStudent', 'aimStudent') - .getOne() + .getOne(); } public findWithRelations = () => { @@ -20,10 +21,10 @@ export class ExchangeRepository extends Repository { .innerJoinAndSelect('exchange.desiredTimeslot', 'desiredTimeslot') .innerJoinAndSelect('exchange.suggesterStudent', 'suggesterStudent') .innerJoinAndSelect('exchange.aimStudent', 'aimStudent') - .getMany() + .getMany(); } - public findByStatus = (status: exchangeStatus) => { + public findByStatus = (status: ExchangeStatus) => { return this.createQueryBuilder("exchange") .where("exchange.status = :status", { status }) .getMany(); diff --git a/src/repository/TimeSlotRepository.ts b/src/repository/TimeSlotRepository.ts index 92e3c85..029ac46 100644 --- a/src/repository/TimeSlotRepository.ts +++ b/src/repository/TimeSlotRepository.ts @@ -7,7 +7,7 @@ export class TimeSlotRepository extends Repository { public findById = (id: number) => { return this.createQueryBuilder("timeSlots") .innerJoinAndSelect("timeSlots.course", "course") - .innerJoinAndSelect("timeSlots.users", "user") + .innerJoinAndSelect("timeSlots.users", "users") .where("timeSlots.id = :id", { id }) .getOne() } diff --git a/src/services/ExchangeService.ts b/src/services/ExchangeService.ts index 48fb645..59b7501 100644 --- a/src/services/ExchangeService.ts +++ b/src/services/ExchangeService.ts @@ -1,5 +1,8 @@ import { getCustomRepository } from "typeorm"; +import { Exchange, ExchangeStatus } from "../entity/Exchange"; +import { User } from "../entity/User"; import { ExchangeRepository } from "../repository/ExchangeRepository"; +import { TimeSlotRepository } from "../repository/TimeSlotRepository"; import CallBack from "./FunctionStatusCode"; import Logger from "./Logger"; @@ -9,9 +12,11 @@ import Logger from "./Logger"; export class ExchangeService { private exchangeRepository: ExchangeRepository; + private timeSlotRepository: TimeSlotRepository; constructor() { this.exchangeRepository = getCustomRepository(ExchangeRepository); + this.timeSlotRepository = getCustomRepository(TimeSlotRepository); } /** @@ -19,7 +24,7 @@ export class ExchangeService { * @param id * @returns Exchange | undefined */ - public findUser = async (id: number) => { + public findById = async (id: number) => { const exchange = await this.exchangeRepository.findOneWithRelations(id); return exchange; } @@ -33,6 +38,170 @@ export class ExchangeService { return exchanges; } + /** + * Exchange + * @param exchange + * @returns boolean + */ + private swapTimeslot = async (exchange: Exchange) => { + let oldExchange = exchange; + + // Validate the exchange in DB + let exchangedTimeslot = await this.timeSlotRepository.findById(exchange.exchangedTimeslot.id); + let desiredTimeslot = await this.timeSlotRepository.findById(exchange.desiredTimeslot.id); + + if (exchangedTimeslot !== undefined && desiredTimeslot !== undefined) { + // Remove the suggester student his old time slot and add the new one + exchangedTimeslot.users = exchangedTimeslot.users.filter( + (user: User) => user !== exchange.suggesterStudent + ); + exchangedTimeslot.users.push(exchange.aimStudent); + + // Remove the aim student his old time slot and add the new one + desiredTimeslot.users = desiredTimeslot.users.filter( + (user: User) => user !== exchange.aimStudent + ); + desiredTimeslot.users.push(exchange.suggesterStudent); + + exchangedTimeslot = await this.timeSlotRepository.save(exchangedTimeslot); + desiredTimeslot = await this.timeSlotRepository.save(desiredTimeslot); + + if ((exchangedTimeslot !== undefined || exchangedTimeslot !== null) && + (desiredTimeslot !== undefined || desiredTimeslot !== null)) { + // Success + return true; + } else { + // Error while validating the exchange -> restore before changes + const exchangeResult = await this.exchangeRepository.save(oldExchange); + if (exchangeResult !== undefined) { + Logger.error("Unable to restore the old exchange => id: " + oldExchange); + } + return false; + } + } else { + Logger.error("Unable to swap students exchange"); + return false; + } + } + + /** + * Save the changes for one exchange + * @param exchange + * @returns boolean + */ + private saveExchangeChanges = async (exchange: Exchange) => { + const exchangeResult = await this.exchangeRepository.save(exchange); + if (exchangeResult !== undefined) { + return CallBack.Status.SUCCESS; + } else { + Logger.error("Unable to save exchange updates"); + return CallBack.Status.DB_ERROR; + } + } + + /** + * Validate an exchange + * @param exchange exchange + * @param user User who requested cancel + * @returns number + */ + public validate = async (exchange: Exchange, user: User) => { + try { + if (user.id === exchange.aimStudent.id) { + if (exchange.status === ExchangeStatus.SUGGESTER_STUDENT_ACCEPTED) { + exchange.status = ExchangeStatus.VALIDATED; + if (!await this.swapTimeslot(exchange)){ + return CallBack.Status.DB_ERROR; + } else { + return await this.saveExchangeChanges(exchange); + } + } else if (exchange.status === ExchangeStatus.PENDING) { + exchange.status = ExchangeStatus.AIM_STUDENT_VALIDATED; + return await this.saveExchangeChanges(exchange); + } else { + return CallBack.Status.SUCCESS; + } + } else if (user.id === exchange.suggesterStudent.id) { + if (exchange.status === ExchangeStatus.AIM_STUDENT_VALIDATED) { + exchange.status = ExchangeStatus.VALIDATED; + if (!await this.swapTimeslot(exchange)){ + CallBack.Status.DB_ERROR; + } else { + return await this.saveExchangeChanges(exchange); + } + } else if (exchange.status === ExchangeStatus.PENDING) { + exchange.status = ExchangeStatus.SUGGESTER_STUDENT_ACCEPTED; + return await this.saveExchangeChanges(exchange); + } else { + return CallBack.Status.SUCCESS; + } + } else { + Logger.error("User find is not the requester or the aim one"); + return CallBack.Status.FAILURE; + } + } catch (err) { + Logger.error(err); + return CallBack.Status.DB_ERROR; + } + } + + /** + * Cancel an exchange + * @param exchange exchange + * @param user User who requested cancel + * @returns number + */ + public cancel = async (exchange: Exchange, user: User) => { + try { + if (exchange.status === ExchangeStatus.CANCELLED) { + Logger.info("Cancel exchange called for cancelled exchange"); + return CallBack.Status.SUCCESS; + } + + if (user.id === exchange.aimStudent.id) { + switch(exchange.status) { + case ExchangeStatus.VALIDATED: + exchange.status = ExchangeStatus.AIM_STUDENT_CANCELLED; + return await this.saveExchangeChanges(exchange); + // TODO: Notify suggester client + case ExchangeStatus.SUGGESTER_STUDENT_ACCEPTED: + exchange.status = ExchangeStatus.CANCELLED; + if (!await this.swapTimeslot(exchange)){ + CallBack.Status.DB_ERROR; + } else { + return await this.saveExchangeChanges(exchange); + // TODO: Notify both students + } + default: return CallBack.Status.SUCCESS; + } + } else if (user.id === exchange.suggesterStudent.id) { + switch(exchange.status) { + case ExchangeStatus.VALIDATED: + exchange.status = ExchangeStatus.SUGGESTER_STUDENT_ACCEPTED; + return await this.saveExchangeChanges(exchange); + // TODO: Notify aim client + case ExchangeStatus.AIM_STUDENT_CANCELLED: + exchange.status = ExchangeStatus.CANCELLED; + if (!await this.swapTimeslot(exchange)){ + return CallBack.Status.DB_ERROR; + } else { + return await this.saveExchangeChanges(exchange); + // TODO: Notify both students + } + default: return CallBack.Status.SUCCESS; + } + } else { + Logger.error("User find is not the requester or the aim one"); + CallBack.Status.LOGIC_ERROR; + } + // Error while validating the exchange + return CallBack.Status.FAILURE; + } catch (err) { + Logger.error(err); + return CallBack.Status.DB_ERROR; + } + } + /** * Create a new Exchange entity * @param body Validated body of the request