From 92bd2c0be3c61e20bfb3fac50a38ac8cacf2810b Mon Sep 17 00:00:00 2001 From: Sinan Date: Sun, 15 Sep 2024 09:25:02 +0530 Subject: [PATCH] slot validations moved to usecase --- .../interface/services/IValidatorService.ts | 1 + .../validators/IAppointmentValidator.ts | 7 --- .../src/infrastructure/services/JoiService.ts | 10 +++ .../controllers/slot/SlotController.ts | 43 +------------ .../presentation/routers/slots/SlotsRoutes.ts | 4 +- server/src/use_case/slot/SlotUseCases.ts | 61 +++++++++++++++++-- 6 files changed, 71 insertions(+), 55 deletions(-) delete mode 100644 server/src/domain/interface/validators/IAppointmentValidator.ts diff --git a/server/src/domain/interface/services/IValidatorService.ts b/server/src/domain/interface/services/IValidatorService.ts index dee14a02..7897238e 100644 --- a/server/src/domain/interface/services/IValidatorService.ts +++ b/server/src/domain/interface/services/IValidatorService.ts @@ -5,6 +5,7 @@ export default interface IValidatorService { validateIdFormat(id: string): boolean; validatePhoneNumber(phoneNumber: string): boolean; validateDateFormat(date: string): boolean; + validateTimeFormat(time: string): boolean; validateEnum(field: string, enumValues: string[]): boolean; validatePassword(password: string): boolean; validateBoolean(value: any): boolean; diff --git a/server/src/domain/interface/validators/IAppointmentValidator.ts b/server/src/domain/interface/validators/IAppointmentValidator.ts deleted file mode 100644 index 1c22b9b5..00000000 --- a/server/src/domain/interface/validators/IAppointmentValidator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Joi from "joi"; -import IAppointment from "../../entities/IAppointment"; - -export interface IAppointmentValidator { - createValidate(appointment: IAppointment): Joi.ValidationResult; - updateValidate(appointment: IAppointment): Joi.ValidationResult; -} diff --git a/server/src/infrastructure/services/JoiService.ts b/server/src/infrastructure/services/JoiService.ts index 5ef1e499..31cdcb75 100644 --- a/server/src/infrastructure/services/JoiService.ts +++ b/server/src/infrastructure/services/JoiService.ts @@ -62,6 +62,16 @@ export default class JoiService implements IValidatorService { return true; } + public validateTimeFormat(time: string): boolean { + const schema = Joi.string().pattern(/^([01]\d|2[0-3]):([0-5]\d) (AM|PM)$/); + + const { error } = schema.validate(time); + if (error) { + throw new ValidationError('Invalid time format, must be in "HH:MM AM/PM" format', StatusCode.BadRequest); + } + return true; + } + public validateEnum(field: string, enumValues: string[]): boolean { const schema = Joi.string().valid(...enumValues); const { error } = schema.validate(field); diff --git a/server/src/presentation/controllers/slot/SlotController.ts b/server/src/presentation/controllers/slot/SlotController.ts index dcf57918..510e5b0d 100644 --- a/server/src/presentation/controllers/slot/SlotController.ts +++ b/server/src/presentation/controllers/slot/SlotController.ts @@ -4,29 +4,13 @@ import SlotUseCase from "../../../use_case/slot/SlotUseCases"; import { Days } from "../../../domain/entities/ISlot"; export default class DoctorController { - private timeFormat: RegExp; - constructor(private slotUseCase: SlotUseCase) { - this.timeFormat = /^(0[1-9]|1[0-2]):([0-5][0-9])\s?(AM|PM)$/i; - } + constructor(private slotUseCase: SlotUseCase) { } async createManyByDay(req: CustomRequest, res: Response, next: NextFunction) { try { const doctorId = req.doctor?.id; const { slots, day } = req.body; - if (!slots || !Array.isArray(slots) || slots.length === 0) { - return res.status(StatusCode.BadRequest).json({ message: 'Slots data is required and should be a non-empty array.' }); - } - - if (!Object.values(Days).includes(day)) { - return res.status(StatusCode.BadRequest).json({ message: 'Invalid or missing day.' }); - } - - for (let slot of slots) { - if (!slot.startTime || !this.timeFormat.test(slot.startTime)) { - return res.status(StatusCode.BadRequest).json({ message: `Invalid or missing startTime for slot: ${JSON.stringify(slot)}` }); - } - } await this.slotUseCase.createManyByDay(doctorId!, slots, day); res.status(StatusCode.Created).json({ message: 'Slots created successfully.' }); @@ -39,11 +23,6 @@ export default class DoctorController { try { const doctorId = req.doctor?.id!; const { startTimes } = req.body; - for (let time of startTimes) { - if (!this.timeFormat.test(time)) { - return res.status(StatusCode.BadRequest).json({ message: `Invalid or missing startTime ${time}` }); - } - } await this.slotUseCase.createForAllDays(doctorId, startTimes) res.status(StatusCode.Created).json({ message: "Slots created successfully" }) } catch (error) { @@ -55,20 +34,6 @@ export default class DoctorController { try { const doctorId = req.doctor?.id; const { slots, day } = req.body; - - if (!slots || !Array.isArray(slots) || slots.length === 0) { - return res.status(StatusCode.BadRequest).json({ message: 'Slots data is required and should be a non-empty array.' }); - } - - if (!Object.values(Days).includes(day)) { - return res.status(StatusCode.BadRequest).json({ message: 'Invalid or missing day.' }); - } - for (let slot of slots) { - if (!slot.startTime || !this.timeFormat.test(slot.startTime)) { - return res.status(StatusCode.BadRequest).json({ message: `Invalid or missing startTime for slot: ${JSON.stringify(slot)}` }); - } - } - await this.slotUseCase.deleteManyByDay(doctorId!, slots, day); res.status(StatusCode.Success).json({ message: "Slots Deleted successfully" }) @@ -81,11 +46,7 @@ export default class DoctorController { try { const doctorId = req.doctor?.id!; const { startTimes } = req.body; - for (let time of startTimes) { - if (!this.timeFormat.test(time)) { - return res.status(StatusCode.BadRequest).json({ message: `Invalid or missing startTime ${time}` }); - } - } + await this.slotUseCase.deleteForAllDays(doctorId, startTimes) res.status(StatusCode.Success).json({ message: "Slots Deleted successfully" }) } catch (error) { diff --git a/server/src/presentation/routers/slots/SlotsRoutes.ts b/server/src/presentation/routers/slots/SlotsRoutes.ts index 3c0d723f..3095ab6d 100644 --- a/server/src/presentation/routers/slots/SlotsRoutes.ts +++ b/server/src/presentation/routers/slots/SlotsRoutes.ts @@ -5,6 +5,7 @@ import SlotController from '../../controllers/slot/SlotController'; import DoctorAuthMiddleware from '../../middlewares/DoctorAuthMiddleware'; import TokenService from '../../../infrastructure/services/JWTService'; import AppointmentRepository from '../../../infrastructure/repositories/AppointmentRepository'; +import JoiService from '../../../infrastructure/services/JoiService'; const router = express.Router(); @@ -12,8 +13,9 @@ const slotRepository = new SlotRepository(); const tokenService = new TokenService(); const authorizeDoctor = new DoctorAuthMiddleware(tokenService); const appointmentRepository = new AppointmentRepository() +const validatorService = new JoiService() -const slotUseCase = new SlotUseCase(slotRepository,appointmentRepository); +const slotUseCase = new SlotUseCase(slotRepository, appointmentRepository, validatorService); const slotController = new SlotController(slotUseCase); router.post('/day', authorizeDoctor.exec.bind(authorizeDoctor), slotController.createManyByDay.bind(slotController)); diff --git a/server/src/use_case/slot/SlotUseCases.ts b/server/src/use_case/slot/SlotUseCases.ts index 6c51cc95..0e5c452c 100644 --- a/server/src/use_case/slot/SlotUseCases.ts +++ b/server/src/use_case/slot/SlotUseCases.ts @@ -1,7 +1,7 @@ -import ISlot, { SlotStatus } from "../../domain/entities/ISlot"; +import ISlot, { SlotStatus, Days } from "../../domain/entities/ISlot"; import ISlotRepository from "../../domain/interface/repositories/ISlotRepository"; -import { Days } from "../../domain/entities/ISlot"; import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository"; +import IValidatorService from "../../domain/interface/services/IValidatorService"; import { AppointmentStatus } from "../../domain/entities/IAppointment"; export default class SlotUseCase { @@ -9,12 +9,17 @@ export default class SlotUseCase { constructor( private slotRepository: ISlotRepository, - private appointmentRepository: IAppointmentRepository + private appointmentRepository: IAppointmentRepository, + private validatorService: IValidatorService ) { this.interval = 1; } + async createManyByDay(doctorId: string, slots: ISlot[], day: Days): Promise { + this.validateSlotStartTimes(slots); + this.validateDay(day); + const existingSlots = await this.slotRepository.findManyByDay(doctorId, day); const newSlots = slots .filter(slot => !existingSlots?.some(existing => existing.startTime === slot.startTime)) @@ -32,6 +37,11 @@ export default class SlotUseCase { } async createForAllDays(doctorId: string, startTimes: string[]): Promise { + startTimes.forEach(time => { + this.validatorService.validateTimeFormat(time); + this.validatorService.validateLength(time, 7, 11); + }); + const days = Object.values(Days); const slotsByDay = days.reduce((acc, day) => { acc[day] = startTimes.map(startTime => ({ @@ -47,6 +57,9 @@ export default class SlotUseCase { } async deleteManyByDay(doctorId: string, slots: ISlot[], day: Days): Promise { + this.validateSlotStartTimes(slots); + this.validateDay(day); + const startTimes = slots.map(el => el.startTime!); const bookedSlots = await this.slotRepository.findManyByDay(doctorId, day, 'booked'); @@ -67,6 +80,11 @@ export default class SlotUseCase { } async deleteForAllDays(doctorId: string, startTimes: string[]): Promise { + startTimes.forEach(time => { + this.validatorService.validateTimeFormat(time); + this.validatorService.validateLength(time, 7, 11); + }); + const days = Object.values(Days); const slots = await this.slotRepository.findManyByDaysAndTimes(doctorId, days, startTimes); @@ -87,18 +105,24 @@ export default class SlotUseCase { } async update(slot: ISlot): Promise { + this.validatorService.validateIdFormat(slot._id!); + this.validatorService.validateTimeFormat(slot.startTime!); await this.slotRepository.update(slot); } async getAllSlots(doctorId: string): Promise { + this.validatorService.validateIdFormat(doctorId); return await this.slotRepository.findMany(doctorId); } async getSlotsByDay(doctorId: string, day: Days): Promise { + this.validateDay(day); + this.validatorService.validateIdFormat(doctorId); return await this.slotRepository.findManyByDay(doctorId, day); } async getSlotsByDate(doctorId: string, date: string): Promise { + this.validatorService.validateIdFormat(doctorId); const day = this.getDayFromDate(date); return await this.slotRepository.findManyByDay(doctorId, day, "available"); } @@ -109,14 +133,39 @@ export default class SlotUseCase { return dayNames[dayOfWeek] as Days; } + private validateSlotStartTimes(slots: ISlot[]): void { + slots.forEach(slot => { + if (!slot.startTime) { + throw new Error(`Missing startTime for slot: ${JSON.stringify(slot)}`); + } + this.validatorService.validateTimeFormat(slot.startTime); + this.validatorService.validateLength(slot.startTime, 7, 11); + }); + } + + private validateDay(day: Days): void { + if (!Object.values(Days).includes(day)) { + throw new Error('Invalid or missing day.'); + } + } + private calculateEndTime(startTime: string): string { - const [hoursStr, minutesStr] = startTime.split(":"); - const hours = parseInt(hoursStr, 10); + const [time, period] = startTime.split(' '); + const [hoursStr, minutesStr] = time.split(":"); + let hours = parseInt(hoursStr, 10); const minutes = parseInt(minutesStr, 10); + + if (period === 'PM' && hours < 12) hours += 12; + if (period === 'AM' && hours === 12) hours = 0; + if (isNaN(hours) || isNaN(minutes)) { throw new Error("Invalid start time format"); } + const endHour = (hours + this.interval) % 24; - return `${endHour.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + const endPeriod = endHour >= 12 ? 'PM' : 'AM'; + const displayHour = endHour % 12 || 12; + + return `${displayHour.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')} ${endPeriod}`; } }