From 3612e132d353ffab827b1184710f136ddaeb9a45 Mon Sep 17 00:00:00 2001 From: Sinan Date: Sun, 15 Sep 2024 14:57:14 +0530 Subject: [PATCH] appointment creation and slot fecthing for appointment logics updated --- server/src/domain/entities/IAppointment.ts | 1 + .../repositories/IAppointmentRepository.ts | 6 ++++-- .../interface/repositories/ISlotRepository.ts | 3 ++- .../infrastructure/database/isValidObjId.ts | 5 ----- .../repositories/AppointmentRepository.ts | 18 ++++++++++------ .../repositories/DoctorRepository.ts | 4 ---- .../repositories/PatientRepository.ts | 5 ----- .../repositories/SlotRepository.ts | 10 +++++++-- .../src/infrastructure/services/JoiService.ts | 4 ++-- .../doctor/AuthenticationController.ts | 1 - .../validators/AppointmentValidator.ts | 21 ------------------- .../validators/entitieValidators.ts | 14 ------------- .../appointment/AppointmentUseCase.ts | 13 +++++++++--- server/src/use_case/slot/SlotUseCases.ts | 13 ++++++++---- 14 files changed, 48 insertions(+), 70 deletions(-) delete mode 100644 server/src/infrastructure/database/isValidObjId.ts delete mode 100644 server/src/presentation/validators/AppointmentValidator.ts delete mode 100644 server/src/presentation/validators/entitieValidators.ts diff --git a/server/src/domain/entities/IAppointment.ts b/server/src/domain/entities/IAppointment.ts index e0e478f7..14362503 100644 --- a/server/src/domain/entities/IAppointment.ts +++ b/server/src/domain/entities/IAppointment.ts @@ -1,4 +1,5 @@ export enum AppointmentStatus { + PAYMENT_PENDING = 'payment-pending', PENDING = 'pending', CONFIRMED = 'confirmed', CANCELLED = 'cancelled', diff --git a/server/src/domain/interface/repositories/IAppointmentRepository.ts b/server/src/domain/interface/repositories/IAppointmentRepository.ts index 824d7dde..ae4ee078 100644 --- a/server/src/domain/interface/repositories/IAppointmentRepository.ts +++ b/server/src/domain/interface/repositories/IAppointmentRepository.ts @@ -4,6 +4,8 @@ export default interface IAppointmentRepository { create(appointment: IAppointment): Promise; update(appointment: IAppointment): Promise; updateStatusMany(appointmentIds: string[], status: AppointmentStatus): Promise; + updateManyBySlotIds(slotIds: string[], fields: IAppointment): Promise; findOneBySlotId(slotId: string): Promise; - updateManyBySlotIds(slotIds: string[],fields:IAppointment): Promise; -} \ No newline at end of file + findByDateAndSlot(appointmentDate: string, slotId:string): Promise; + findManyByDateAndDoctorId(appointmentDate:string,doctorId:string):Promise; +} diff --git a/server/src/domain/interface/repositories/ISlotRepository.ts b/server/src/domain/interface/repositories/ISlotRepository.ts index f372b4d2..780e18d3 100644 --- a/server/src/domain/interface/repositories/ISlotRepository.ts +++ b/server/src/domain/interface/repositories/ISlotRepository.ts @@ -4,9 +4,10 @@ export default interface ISlotRepository { update(slot: ISlot): Promise; createMany(slots: ISlot[]): Promise; deleteManyByDayAndTime(doctorId: string, day: Days, startTimes: string[]): Promise + deleteManyByDaysAndTimes(doctorId:string,days:Days[],startTimes:string[]):Promise; findMany(doctorId: string): Promise; findManyByDay(doctorId: string, day: Days, status?:SlotStatus): Promise; findById(slotId: string): Promise; findManyByDaysAndTimes(doctorId:string,days:Days[],startTimes:string[]):Promise; - deleteManyByDaysAndTimes(doctorId:string,days:Days[],startTimes:string[]):Promise; + findManyNotInSlotIds(doctorId: string, day: Days, excludedSlotIds: string[]): Promise } \ No newline at end of file diff --git a/server/src/infrastructure/database/isValidObjId.ts b/server/src/infrastructure/database/isValidObjId.ts deleted file mode 100644 index 9fbc968a..00000000 --- a/server/src/infrastructure/database/isValidObjId.ts +++ /dev/null @@ -1,5 +0,0 @@ -import mongoose from "mongoose"; - -export const isValidObjectId = (id: string): boolean => { - return mongoose.Types.ObjectId.isValid(id); -}; diff --git a/server/src/infrastructure/repositories/AppointmentRepository.ts b/server/src/infrastructure/repositories/AppointmentRepository.ts index bd847a54..de42ac09 100644 --- a/server/src/infrastructure/repositories/AppointmentRepository.ts +++ b/server/src/infrastructure/repositories/AppointmentRepository.ts @@ -1,7 +1,6 @@ import IAppointment, { AppointmentStatus } from "../../domain/entities/IAppointment"; import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository"; import AppointmentModel from "../database/AppointmentModel"; -import { isValidObjectId } from "../database/isValidObjId"; export default class AppointmentRepository implements IAppointmentRepository { model = AppointmentModel @@ -9,19 +8,26 @@ export default class AppointmentRepository implements IAppointmentRepository { await this.model.create(appointment) } async update(appointment: IAppointment): Promise { - if (!isValidObjectId(appointment._id!)) throw new Error("Invalid Object Id"); - await this.model.findByIdAndUpdate(appointment._id,appointment,{new:true}) + await this.model.findByIdAndUpdate(appointment._id, appointment, { new: true }) } async findOneBySlotId(slotId: string): Promise { - return await this.model.findOne({slotId}); + return await this.model.findOne({ slotId }); } + + async findManyByDateAndDoctorId(appointmentDate: string, doctorId: string): Promise { + return await this.model.find({appointmentDate,doctorId}); + } + async findByDateAndSlot(appointmentDate: string, slotId: string): Promise { + return await this.model.findOne({ appointmentDate, slotId }) + } + async updateStatusMany(appointmentIds: string[], status: AppointmentStatus): Promise { - await this.model.updateMany({_id:{$in:appointmentIds}},{status}) + await this.model.updateMany({ _id: { $in: appointmentIds } }, { status }) } async updateManyBySlotIds(slotIds: string[], fields: Partial): Promise { await this.model.updateMany({ slotId: { $in: slotIds } }, fields); } - + } \ No newline at end of file diff --git a/server/src/infrastructure/repositories/DoctorRepository.ts b/server/src/infrastructure/repositories/DoctorRepository.ts index 05d60b92..41cef422 100644 --- a/server/src/infrastructure/repositories/DoctorRepository.ts +++ b/server/src/infrastructure/repositories/DoctorRepository.ts @@ -2,7 +2,6 @@ import IDoctor from "../../domain/entities/IDoctor"; import IDoctorRepository from "../../domain/interface/repositories/IDoctorRepository"; import DoctorModel from "../database/DoctorModel"; import { Types } from 'mongoose' -import { isValidObjectId } from "../database/isValidObjId"; import { PaginatedResult } from "../../types"; export default class DoctorRepository implements IDoctorRepository { @@ -11,8 +10,6 @@ export default class DoctorRepository implements IDoctorRepository { return await this.model.findOne({ email }).select(["-token", "-password"]); } async findByID(id: string | Types.ObjectId,): Promise { - if (typeof id === 'string' && !isValidObjectId(id)) throw new Error("Invalid Object Id"); - return await this.model.findById(id).select(["-token", "-password"]); } async findByEmailWithCredentials(email: string): Promise { @@ -22,7 +19,6 @@ export default class DoctorRepository implements IDoctorRepository { return (await this.model.create(doctor))._id; } async update(doctor: IDoctor): Promise { - if (!isValidObjectId(doctor._id!)) throw new Error("Invalid Object Id"); return await this.model.findByIdAndUpdate(doctor._id, doctor, { new: true }); } async findMany(offset: number, limit: number, isVerified: boolean, isBlocked: boolean): Promise> { diff --git a/server/src/infrastructure/repositories/PatientRepository.ts b/server/src/infrastructure/repositories/PatientRepository.ts index 8ead4de4..f0672bfb 100644 --- a/server/src/infrastructure/repositories/PatientRepository.ts +++ b/server/src/infrastructure/repositories/PatientRepository.ts @@ -1,7 +1,6 @@ import { IPatient } from "../../domain/entities/IPatient"; import IPatientRepository from "../../domain/interface/repositories/IPatientRepository"; import { PaginatedResult } from "../../types"; -import { isValidObjectId } from "../database/isValidObjId"; import PatientModel from "../database/PatientModel"; export default class PatientRepository implements IPatientRepository { @@ -43,13 +42,9 @@ export default class PatientRepository implements IPatientRepository { return await this.model.findOne({ email }).select(["-password", "-token"]); } async findByIdAndUpdate(id: string, patient: IPatient): Promise { - if (!isValidObjectId(id)) throw new Error("Invalid Object Id"); return await this.model.findByIdAndUpdate(id, patient, { new: true }); } async findById(id: string): Promise { - if (!isValidObjectId(id)) { - throw new Error(`Invalid Object Id`); - } return await this.model.findById(id).select(["-password", "-token"]); } async findByEmailWithCredentials(email: string): Promise { diff --git a/server/src/infrastructure/repositories/SlotRepository.ts b/server/src/infrastructure/repositories/SlotRepository.ts index 18db19f0..f08f147d 100644 --- a/server/src/infrastructure/repositories/SlotRepository.ts +++ b/server/src/infrastructure/repositories/SlotRepository.ts @@ -1,6 +1,5 @@ import ISlot, { Days, SlotStatus } from "../../domain/entities/ISlot"; import ISlotRepository from "../../domain/interface/repositories/ISlotRepository"; -import { isValidObjectId } from "../database/isValidObjId"; import SlotModel from "../database/SlotModel"; export default class SlotRepository implements ISlotRepository { @@ -27,6 +26,14 @@ export default class SlotRepository implements ISlotRepository { startTime: { $in: startTimes } }); } + + async findManyNotInSlotIds(doctorId: string, day: Days, excludedSlotIds: string[]): Promise { + return await this.model.find({ + doctorId, + day, + _id: { $nin: excludedSlotIds } + }); + } async update(slot: ISlot): Promise { await this.model.findByIdAndUpdate(slot._id, slot, { upsert: true }); } @@ -44,7 +51,6 @@ export default class SlotRepository implements ISlotRepository { } async findById(slotId: string): Promise { - if (!isValidObjectId(slotId)) throw new Error("Invalid Object Id") return await this.model.findById(slotId) } diff --git a/server/src/infrastructure/services/JoiService.ts b/server/src/infrastructure/services/JoiService.ts index 2411f78b..ed5f6c4e 100644 --- a/server/src/infrastructure/services/JoiService.ts +++ b/server/src/infrastructure/services/JoiService.ts @@ -64,7 +64,7 @@ export default class JoiService implements IValidatorService { 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); @@ -87,7 +87,7 @@ export default class JoiService implements IValidatorService { if (error) { throw new ValidationError( 'Password must be at least 8 characters, include at least one uppercase letter, one number, and one special character', - StatusCode.BadRequest + StatusCode.UnprocessableEntity ); } return true; diff --git a/server/src/presentation/controllers/doctor/AuthenticationController.ts b/server/src/presentation/controllers/doctor/AuthenticationController.ts index 34b8e7a3..6f8d56f1 100644 --- a/server/src/presentation/controllers/doctor/AuthenticationController.ts +++ b/server/src/presentation/controllers/doctor/AuthenticationController.ts @@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from "express"; import AuthenticationUseCase from "../../../use_case/doctor/AuthenticationUseCase"; import { Cookie, StatusCode } from "../../../types"; import IDoctor from "../../../domain/entities/IDoctor"; -import { isValidatePassword, isValidEmail } from "../../validators/entitieValidators"; export default class AuthDoctorController { constructor(private authDoctorUseCase: AuthenticationUseCase) {} diff --git a/server/src/presentation/validators/AppointmentValidator.ts b/server/src/presentation/validators/AppointmentValidator.ts deleted file mode 100644 index 29c7d3a4..00000000 --- a/server/src/presentation/validators/AppointmentValidator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Joi from "joi"; -import IAppointment, { AppointmentType } from "../../domain/entities/IAppointment"; - -export default class AppointmentValidator { - private schema: Joi.ObjectSchema; - - constructor() { - this.schema = Joi.object({ - doctorId: Joi.string().required(), - slotId: Joi.string().required(), - appointmentType: Joi.string().valid(...Object.values(AppointmentType)).required(), - appointmentDate: Joi.string().isoDate().required(), - reason: Joi.string().optional(), - notes: Joi.string().optional(), - }); - } - - validate(appointment: IAppointment) { - return this.schema.validate(appointment); - } -} diff --git a/server/src/presentation/validators/entitieValidators.ts b/server/src/presentation/validators/entitieValidators.ts deleted file mode 100644 index 1b48f9bb..00000000 --- a/server/src/presentation/validators/entitieValidators.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const isValidEmail = (email: string): boolean => { - const allowedDomains = ["gmail.com", "outlook.com", "icloud.com", "yahoo.com"]; - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return false; - } - const domain = email.split("@")[1]; - return allowedDomains.includes(domain); -}; - -export const isValidatePassword = (password: string): boolean => { - const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$/; - return passwordRegex.test(password); -}; diff --git a/server/src/use_case/appointment/AppointmentUseCase.ts b/server/src/use_case/appointment/AppointmentUseCase.ts index 7951c4fe..f3431417 100644 --- a/server/src/use_case/appointment/AppointmentUseCase.ts +++ b/server/src/use_case/appointment/AppointmentUseCase.ts @@ -1,7 +1,9 @@ import IAppointment, { AppointmentStatus, AppointmentType } from "../../domain/entities/IAppointment"; +import { ValidationError } from "../../domain/entities/ValidationError"; import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository"; import ISlotRepository from "../../domain/interface/repositories/ISlotRepository"; import IValidatorService from "../../domain/interface/services/IValidatorService"; +import { StatusCode } from "../../types"; export default class AppointmentUseCase { constructor( @@ -21,9 +23,14 @@ export default class AppointmentUseCase { if (notes) this.validatorService.validateLength(notes, 0, 255); const slot = await this.slotRepository.findById(slotId!); - if (!slot) throw new Error("Slot Not Found"); - if (slot.status === 'booked') throw new Error("Slot already booked"); - slot!.status = 'booked'; + if (!slot) throw new ValidationError("Slot Not Found", StatusCode.NotFound); + + if (slot.status === 'booked') { + const bookedAppointment = await this.appointRepository.findByDateAndSlot(appointmentDate!, slotId!); + if (bookedAppointment) throw new ValidationError("Slot already booked", StatusCode.Conflict); + } else { + slot!.status = 'booked'; + } await this.slotRepository.update(slot); await this.appointRepository.create({ slotId, diff --git a/server/src/use_case/slot/SlotUseCases.ts b/server/src/use_case/slot/SlotUseCases.ts index 0e5c452c..5e00bde7 100644 --- a/server/src/use_case/slot/SlotUseCases.ts +++ b/server/src/use_case/slot/SlotUseCases.ts @@ -3,6 +3,8 @@ import ISlotRepository from "../../domain/interface/repositories/ISlotRepository import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository"; import IValidatorService from "../../domain/interface/services/IValidatorService"; import { AppointmentStatus } from "../../domain/entities/IAppointment"; +import { ValidationError } from "../../domain/entities/ValidationError"; +import { StatusCode } from "../../types"; export default class SlotUseCase { protected interval: number; @@ -123,10 +125,13 @@ export default class SlotUseCase { async getSlotsByDate(doctorId: string, date: string): Promise { this.validatorService.validateIdFormat(doctorId); + const appointments = await this.appointmentRepository.findManyByDateAndDoctorId(date, doctorId); + const slotIds = appointments?.map(el => el.slotId!) || []; const day = this.getDayFromDate(date); - return await this.slotRepository.findManyByDay(doctorId, day, "available"); + return await this.slotRepository.findManyNotInSlotIds(doctorId, day, slotIds); } + private getDayFromDate(date: string): Days { const dayOfWeek = new Date(date).getUTCDay(); const dayNames = Object.values(Days); @@ -136,7 +141,7 @@ export default class SlotUseCase { private validateSlotStartTimes(slots: ISlot[]): void { slots.forEach(slot => { if (!slot.startTime) { - throw new Error(`Missing startTime for slot: ${JSON.stringify(slot)}`); + throw new ValidationError(`Missing startTime for slot: ${JSON.stringify(slot)}`,StatusCode.BadRequest); } this.validatorService.validateTimeFormat(slot.startTime); this.validatorService.validateLength(slot.startTime, 7, 11); @@ -145,7 +150,7 @@ export default class SlotUseCase { private validateDay(day: Days): void { if (!Object.values(Days).includes(day)) { - throw new Error('Invalid or missing day.'); + throw new ValidationError('Invalid or missing day.',StatusCode.BadRequest); } } @@ -159,7 +164,7 @@ export default class SlotUseCase { if (period === 'AM' && hours === 12) hours = 0; if (isNaN(hours) || isNaN(minutes)) { - throw new Error("Invalid start time format"); + throw new ValidationError("Invalid start time format",StatusCode.BadRequest); } const endHour = (hours + this.interval) % 24;