Skip to content

Commit

Permalink
slot validations moved to usecase
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 15, 2024
1 parent a0a6d55 commit 92bd2c0
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 55 deletions.
1 change: 1 addition & 0 deletions server/src/domain/interface/services/IValidatorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

This file was deleted.

10 changes: 10 additions & 0 deletions server/src/infrastructure/services/JoiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 2 additions & 41 deletions server/src/presentation/controllers/slot/SlotController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.' });

Expand All @@ -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) {
Expand All @@ -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" })
Expand All @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion server/src/presentation/routers/slots/SlotsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ 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();

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));
Expand Down
61 changes: 55 additions & 6 deletions server/src/use_case/slot/SlotUseCases.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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 {
protected interval: number;

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<void> {
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))
Expand All @@ -32,6 +37,11 @@ export default class SlotUseCase {
}

async createForAllDays(doctorId: string, startTimes: string[]): Promise<void> {
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 => ({
Expand All @@ -47,6 +57,9 @@ export default class SlotUseCase {
}

async deleteManyByDay(doctorId: string, slots: ISlot[], day: Days): Promise<void> {
this.validateSlotStartTimes(slots);
this.validateDay(day);

const startTimes = slots.map(el => el.startTime!);

const bookedSlots = await this.slotRepository.findManyByDay(doctorId, day, 'booked');
Expand All @@ -67,6 +80,11 @@ export default class SlotUseCase {
}

async deleteForAllDays(doctorId: string, startTimes: string[]): Promise<void> {
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);
Expand All @@ -87,18 +105,24 @@ export default class SlotUseCase {
}

async update(slot: ISlot): Promise<void> {
this.validatorService.validateIdFormat(slot._id!);
this.validatorService.validateTimeFormat(slot.startTime!);
await this.slotRepository.update(slot);
}

async getAllSlots(doctorId: string): Promise<ISlot[] | null> {
this.validatorService.validateIdFormat(doctorId);
return await this.slotRepository.findMany(doctorId);
}

async getSlotsByDay(doctorId: string, day: Days): Promise<ISlot[] | null> {
this.validateDay(day);
this.validatorService.validateIdFormat(doctorId);
return await this.slotRepository.findManyByDay(doctorId, day);
}

async getSlotsByDate(doctorId: string, date: string): Promise<ISlot[] | null> {
this.validatorService.validateIdFormat(doctorId);
const day = this.getDayFromDate(date);
return await this.slotRepository.findManyByDay(doctorId, day, "available");
}
Expand All @@ -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}`;
}
}

1 comment on commit 92bd2c0

@vercel
Copy link

@vercel vercel bot commented on 92bd2c0 Sep 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.