From b62dd03df2853ba529bbce8817d776a481866e43 Mon Sep 17 00:00:00 2001 From: Sinan Date: Fri, 27 Sep 2024 08:31:22 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20on=20appointment=20=20videoSecti?= =?UTF-8?q?on=20creation=20completed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(patient)/new-appointment/[id]/page.tsx | 14 ++--- .../src/domain/entities/IVideoChatSection.ts | 22 +++---- .../repositories/IAppointmentRepository.ts | 2 +- .../repositories/IDoctorRepository.ts | 2 +- .../infrastructure/model/VideoSectionModel.ts | 2 + .../repositories/AppointmentRepository.ts | 4 +- .../repositories/DoctorRepository.ts | 2 +- .../routers/appointment/AppointmentRoutes.ts | 13 +++- .../appointment/CreateAppointmentUseCase.ts | 59 ++++++++++++++++--- server/src/use_case/chat/CreateChatUseCase.ts | 2 +- .../use_case/doctor/AuthenticationUseCase.ts | 6 +- server/src/use_case/slot/CreateSlotUseCase.ts | 21 ++----- server/src/utils/date-formatter.ts | 8 +++ 13 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 server/src/utils/date-formatter.ts diff --git a/client/app/(patient)/new-appointment/[id]/page.tsx b/client/app/(patient)/new-appointment/[id]/page.tsx index bd7ba2f4..186df014 100644 --- a/client/app/(patient)/new-appointment/[id]/page.tsx +++ b/client/app/(patient)/new-appointment/[id]/page.tsx @@ -6,11 +6,11 @@ import Image from "next/image"; import Link from "next/link"; import { useParams } from "next/navigation"; import { XCircle, Calendar, User, Stethoscope } from "lucide-react"; -import { Button } from "@/components/ui/button"; import { format } from 'date-fns'; import { BreadcrumbCollapsed } from "@/components/navigation/BreadCrumbs"; import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import { motion } from "framer-motion"; +import { ButtonV2 } from "@/components/button/ButtonV2"; export default function AppointmentSuccessPage() { const paymentId = useParams().id as string; @@ -47,9 +47,9 @@ export default function AppointmentSuccessPage() { {error?.response?.data?.message || "We couldn't fetch your appointment details. Please try again later."}

- + ) : ( <> @@ -143,12 +143,12 @@ export default function AppointmentSuccessPage() { {!isLoading && !error && ( - - + )} diff --git a/server/src/domain/entities/IVideoChatSection.ts b/server/src/domain/entities/IVideoChatSection.ts index 7d304451..08528c95 100644 --- a/server/src/domain/entities/IVideoChatSection.ts +++ b/server/src/domain/entities/IVideoChatSection.ts @@ -1,14 +1,16 @@ export default interface IVideoSection { - _id: string; - appointmentId: string; - doctorName: string; - patientName: string; - createdAt: Date; - updatedAt: Date; - startTime: Date | string; - endTime: Date | string; - link: string; - status: VideoSectionStatus; + _id?: string; + appointmentId?: string; + doctorName?: string; + patientName?: string; + doctorProfile?: string; + patientProfile?: string; + createdAt?: Date; + updatedAt?: Date; + startTime?: Date | string; + endTime?: Date | string; + link?: string; + status?: VideoSectionStatus; } export enum VideoSectionStatus { diff --git a/server/src/domain/interface/repositories/IAppointmentRepository.ts b/server/src/domain/interface/repositories/IAppointmentRepository.ts index 4c51bfee..5ae01643 100644 --- a/server/src/domain/interface/repositories/IAppointmentRepository.ts +++ b/server/src/domain/interface/repositories/IAppointmentRepository.ts @@ -2,7 +2,7 @@ import { PaginatedResult } from "../../../types"; import IAppointment, { AppointmentStatus, IExtendedAppointment } from "../../entities/IAppointment"; export default interface IAppointmentRepository { - create(appointment: IAppointment): Promise; + create(appointment: IAppointment): Promise; update(appointment: IAppointment): Promise; updateManyBySlotIdsNotInStatuses(slotIds: string[], fields: IAppointment, notInStatuses:AppointmentStatus[]): Promise; findByDateAndSlot(appointmentDate: string, slotId: string): Promise; diff --git a/server/src/domain/interface/repositories/IDoctorRepository.ts b/server/src/domain/interface/repositories/IDoctorRepository.ts index b756d01b..2952eebd 100644 --- a/server/src/domain/interface/repositories/IDoctorRepository.ts +++ b/server/src/domain/interface/repositories/IDoctorRepository.ts @@ -4,7 +4,7 @@ import { PaginatedResult } from "../../../types"; export default interface IDoctorRepository { findByEmail(email: string): Promise; - findByID(id: string | Types.ObjectId): Promise; + findById(id: string | Types.ObjectId): Promise; findByEmailWithCredentials(email: string): Promise; create(doctor: IDoctor): Promise; update(doctor: IDoctor): Promise; diff --git a/server/src/infrastructure/model/VideoSectionModel.ts b/server/src/infrastructure/model/VideoSectionModel.ts index b04659ec..8ac2ec6e 100644 --- a/server/src/infrastructure/model/VideoSectionModel.ts +++ b/server/src/infrastructure/model/VideoSectionModel.ts @@ -16,6 +16,8 @@ const videoSectionSchema = new Schema({ enum: Object.values(VideoSectionStatus), default: VideoSectionStatus.PENDING }, + doctorProfile: { type: String, required: true }, + patientProfile: { type: String, required: true }, }) const VideoSectionModel = model('VideoSection', videoSectionSchema); diff --git a/server/src/infrastructure/repositories/AppointmentRepository.ts b/server/src/infrastructure/repositories/AppointmentRepository.ts index d79b131b..4a38c162 100644 --- a/server/src/infrastructure/repositories/AppointmentRepository.ts +++ b/server/src/infrastructure/repositories/AppointmentRepository.ts @@ -8,8 +8,8 @@ import { getPaginatedResult } from "./getPaginatedResult"; export default class AppointmentRepository implements IAppointmentRepository { model = AppointmentModel; - async create(appointment: IAppointment): Promise { - return (await this.model.create(appointment))._id; + async create(appointment: IAppointment): Promise { + return (await this.model.create(appointment)); } async update(appointment: IAppointment): Promise { return await this.model.findByIdAndUpdate(appointment._id, appointment, { new: true }); diff --git a/server/src/infrastructure/repositories/DoctorRepository.ts b/server/src/infrastructure/repositories/DoctorRepository.ts index 1ab1b83c..5d9b9c5b 100644 --- a/server/src/infrastructure/repositories/DoctorRepository.ts +++ b/server/src/infrastructure/repositories/DoctorRepository.ts @@ -10,7 +10,7 @@ export default class DoctorRepository implements IDoctorRepository { async findByEmail(email: string): Promise { return await this.model.findOne({ email }).select(["-token", "-password"]); } - async findByID(id: string | Types.ObjectId): Promise { + async findById(id: string | Types.ObjectId): Promise { return await this.model.findById(id).select(["-token", "-password"]); } async findByEmailWithCredentials(email: string): Promise { diff --git a/server/src/presentation/routers/appointment/AppointmentRoutes.ts b/server/src/presentation/routers/appointment/AppointmentRoutes.ts index 666d5b2d..78b1b0ad 100644 --- a/server/src/presentation/routers/appointment/AppointmentRoutes.ts +++ b/server/src/presentation/routers/appointment/AppointmentRoutes.ts @@ -12,6 +12,9 @@ import GetAppointmentUseCase from "../../../use_case/appointment/GetAppointmentU import DoctorAuthMiddleware from "../../middlewares/DoctorAuthMiddleware"; import UpdateAppointmentUseCase from "../../../use_case/appointment/UpdateAppointmentUseCase"; import NotificationRepository from "../../../infrastructure/repositories/NotificationRepository"; +import VideoSectionRepository from "../../../infrastructure/repositories/VideoSectionRepository"; +import PatientRepository from "../../../infrastructure/repositories/PatientRepository"; +import DoctorRepository from "../../../infrastructure/repositories/DoctorRepository"; const router = express.Router(); @@ -22,14 +25,20 @@ const validatorService = new JoiService(); const paymentService = new StripeService(); const paymentRepository = new PaymentRepository(); -const notificationRepository = new NotificationRepository() +const notificationRepository = new NotificationRepository(); +const videoSectionRepository = new VideoSectionRepository(); +const patientRepository = new PatientRepository(); +const doctorRepository = new DoctorRepository(); const createAppointmentUseCase = new CreateAppointmentUseCase( appointmentRepository, slotRepository, validatorService, paymentService, - paymentRepository + paymentRepository, + videoSectionRepository, + doctorRepository, + patientRepository ); const getAppointmentUseCase = new GetAppointmentUseCase(appointmentRepository, validatorService, paymentRepository); const updateAppointmentUseCase = new UpdateAppointmentUseCase(appointmentRepository, validatorService, notificationRepository); diff --git a/server/src/use_case/appointment/CreateAppointmentUseCase.ts b/server/src/use_case/appointment/CreateAppointmentUseCase.ts index d84154ef..9a87ec26 100644 --- a/server/src/use_case/appointment/CreateAppointmentUseCase.ts +++ b/server/src/use_case/appointment/CreateAppointmentUseCase.ts @@ -7,6 +7,13 @@ import IPaymentRepository from "../../domain/interface/repositories/IPaymentRepo import IPayment, { PaymentStatus } from "../../domain/entities/IPayment"; import IAppointment, { AppointmentStatus, AppointmentType } from "../../domain/entities/IAppointment"; import { StatusCode } from "../../types"; +import { IVideoSectionRepository } from "../../domain/interface/repositories/IVideoSectionRepository"; +import IPatientRepository from "../../domain/interface/repositories/IPatientRepository"; +import IDoctorRepository from "../../domain/interface/repositories/IDoctorRepository"; +import IDoctor from "../../domain/entities/IDoctor"; +import IPatient from "../../domain/entities/IPatient"; +import { VideoSectionStatus } from "../../domain/entities/IVideoChatSection"; +import { addMinutes, parse, format } from "../../utils/date-formatter"; export default class AppointmentUseCase { bookingAmount: number; @@ -16,7 +23,10 @@ export default class AppointmentUseCase { private slotRepository: ISlotRepository, private validatorService: IValidatorService, private paymentService: IPaymentService, - private paymentRepository: IPaymentRepository + private paymentRepository: IPaymentRepository, + private videoSectionRepository: IVideoSectionRepository, + private doctorRepository: IDoctorRepository, + private patientRepository: IPatientRepository ) { this.bookingAmount = 300; } @@ -24,6 +34,11 @@ export default class AppointmentUseCase { async exec(appointmentData: IAppointment, patientId: string): Promise<{ sessionId: string; checkoutUrl: string }> { this.validateAppointmentData(appointmentData, patientId); + const patient = await this.patientRepository.findById(patientId); + const doctor = await this.doctorRepository.findById(appointmentData.doctorId!); + + if (!patient || !doctor) throw new CustomError("Patient or Doctor Not Found", StatusCode.NotFound); + const slot = await this.slotRepository.findById(appointmentData.slotId!); if (!slot) throw new CustomError("Slot Not Found", StatusCode.NotFound); @@ -54,7 +69,7 @@ export default class AppointmentUseCase { { paymentId: payment._id?.toString() } ); - const appointmentId = await this.appointmentRepository.create({ + const appointment = await this.appointmentRepository.create({ ...appointmentData, patientId, status: AppointmentStatus.PAYMENT_PENDING, @@ -64,30 +79,56 @@ export default class AppointmentUseCase { await this.paymentRepository.update({ _id: payment._id, orderId: checkoutSession.id, - appointmentId, + appointmentId: appointment._id, }); + await this.createVideoSection(appointment, patient!, doctor!, slot.startTime!) + return { sessionId: checkoutSession.id, checkoutUrl: checkoutSession.url! }; } + private async createVideoSection(appointment: IAppointment, patient: IPatient, doctor: IDoctor, slotStartTime: string): Promise { + const appointmentDate = appointment.appointmentDate as string; + const slotStartTimeFormatted = parse(slotStartTime, 'hh:mm a', new Date(appointmentDate)); + + const appointmentDurationMinutes = 60; + const slotEndTime = addMinutes(slotStartTimeFormatted, appointmentDurationMinutes); + + const calculatedStartTime = format(slotStartTimeFormatted, "yyyy-MM-dd'T'HH:mm:ssXXX"); + const calculatedEndTime = format(slotEndTime, "yyyy-MM-dd'T'HH:mm:ssXXX"); + + await this.videoSectionRepository.create({ + appointmentId: appointment._id!, + patientName: patient.name, + doctorName: doctor.name, + patientProfile: patient.profile, + doctorProfile: doctor.image, + startTime: calculatedStartTime, + endTime: calculatedEndTime, + createdAt: appointment.createdAt as unknown as Date, + updatedAt: appointment.updatedAt as unknown as Date, + status: VideoSectionStatus.PENDING + }); + } + async handleStripeWebhook(body: Buffer, signature: string): Promise { const result = await this.paymentService.handleWebhookEvent(body, signature); const { event, transactionId } = result; - + if (!event || !event.data || !event.data.object) { return; } const paymentIntentMetadata = event.data.object.metadata as { paymentId: string }; - + if (!paymentIntentMetadata || !paymentIntentMetadata.paymentId) { return; } - + await this.verifyPaymentIntent(paymentIntentMetadata.paymentId, transactionId); } - - private async verifyPaymentIntent(id: string, transactionId:string): Promise { + + private async verifyPaymentIntent(id: string, transactionId: string): Promise { const payment = await this.paymentRepository.findById(id); if (!payment) { @@ -97,7 +138,7 @@ export default class AppointmentUseCase { await this.paymentRepository.update({ _id: payment._id, status: PaymentStatus.COMPLETED, - paymentId:transactionId + paymentId: transactionId }); await this.appointmentRepository.updateAppointmentStatusToConfirmed(payment.appointmentId!); diff --git a/server/src/use_case/chat/CreateChatUseCase.ts b/server/src/use_case/chat/CreateChatUseCase.ts index 0d8e816f..a742eea0 100644 --- a/server/src/use_case/chat/CreateChatUseCase.ts +++ b/server/src/use_case/chat/CreateChatUseCase.ts @@ -19,7 +19,7 @@ export default class CreateChatUseCase { this.validatorService.validateIdFormat(doctorId); this.validatorService.validateIdFormat(patientId); const patient = await this.patientRepository.findById(patientId); - const doctor = await this.doctorRepository.findByID(doctorId); + const doctor = await this.doctorRepository.findById(doctorId); if (!patient) { throw new CustomError("Invalid patient id", StatusCode.NotFound); } else if (!doctor) { diff --git a/server/src/use_case/doctor/AuthenticationUseCase.ts b/server/src/use_case/doctor/AuthenticationUseCase.ts index c6d7a723..60d7a52a 100644 --- a/server/src/use_case/doctor/AuthenticationUseCase.ts +++ b/server/src/use_case/doctor/AuthenticationUseCase.ts @@ -129,7 +129,7 @@ export default class AuthenticationUseCase { async getPreSignedUrl(id: string): Promise<{ url: string; key: string }> { this.validatorService.validateIdFormat(id); - const doctor = await this.doctorRepository.findByID(id); + const doctor = await this.doctorRepository.findById(id); if (!doctor) throw new CustomError("Not Found", StatusCode.NotFound); const key = `profile-images/doctor/${id}-${Date.now()}`; const url = await this.cloudService.generatePreSignedUrl(process.env.S3_BUCKET_NAME!, key, 30); @@ -138,7 +138,7 @@ export default class AuthenticationUseCase { async updateProfileImage(key: string, id: string): Promise { this.validatorService.validateIdFormat(id); - const doctor = await this.doctorRepository.findByID(id); + const doctor = await this.doctorRepository.findById(id); if (!doctor) throw new CustomError("Not Found", StatusCode.NotFound); if (doctor.isBlocked) throw new CustomError("Doctor is Blocked", StatusCode.Forbidden); @@ -152,7 +152,7 @@ export default class AuthenticationUseCase { async refresh(token: string): Promise<{ accessToken: string }> { const { id } = this.tokenService.verifyRefreshToken(token); - const doctor = await this.doctorRepository.findByID(id); + const doctor = await this.doctorRepository.findById(id); if (!doctor) throw new CustomError("Unauthorized", StatusCode.Unauthorized); if (doctor.isBlocked) throw new CustomError("Doctor is Blocked", StatusCode.Forbidden); diff --git a/server/src/use_case/slot/CreateSlotUseCase.ts b/server/src/use_case/slot/CreateSlotUseCase.ts index b66c8c35..c0fe03ed 100644 --- a/server/src/use_case/slot/CreateSlotUseCase.ts +++ b/server/src/use_case/slot/CreateSlotUseCase.ts @@ -1,3 +1,4 @@ +import { parse, format, addHours } from "../../utils/date-formatter"; import ISlotRepository from "../../domain/interface/repositories/ISlotRepository"; import IValidatorService from "../../domain/interface/services/IValidatorService"; import CustomError from "../../domain/entities/CustomError"; @@ -68,22 +69,8 @@ export default class CreateSlotUseCase { } private calculateEndTime(startTime: string): string { - 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 CustomError("Invalid start time format", StatusCode.BadRequest); - } - - const endHour = (hours + this.interval) % 24; - const endPeriod = endHour >= 12 ? "PM" : "AM"; - const displayHour = endHour % 12 || 12; - - return `${displayHour.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")} ${endPeriod}`; + const parsedStartTime = parse(startTime, "hh:mm a", new Date()); + const endTime = addHours(parsedStartTime, this.interval); + return format(endTime, "hh:mm a"); } } diff --git a/server/src/utils/date-formatter.ts b/server/src/utils/date-formatter.ts new file mode 100644 index 00000000..b1cb6037 --- /dev/null +++ b/server/src/utils/date-formatter.ts @@ -0,0 +1,8 @@ +import { parse, format, addMinutes, addHours } from "date-fns"; + +export { + parse, + format, + addMinutes, + addHours +} \ No newline at end of file