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