Skip to content

Commit

Permalink
🚀 on appointment videoSection creation completed
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 27, 2024
1 parent 45f5eac commit b62dd03
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 54 deletions.
14 changes: 7 additions & 7 deletions client/app/(patient)/new-appointment/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,9 +47,9 @@ export default function AppointmentSuccessPage() {
{error?.response?.data?.message ||
"We couldn't fetch your appointment details. Please try again later."}
</p>
<Button asChild variant="destructive">
<ButtonV2 asChild variant="destructive">
<Link href="/new-appointment">Try Booking Again</Link>
</Button>
</ButtonV2>
</motion.div>
) : (
<>
Expand Down Expand Up @@ -143,12 +143,12 @@ export default function AppointmentSuccessPage() {
</CardContent>
{!isLoading && !error && (
<CardFooter className="flex justify-center space-x-4 p-6 bg-primary/5">
<Button variant="default" asChild>
<ButtonV2 variant="gooeyLeft" asChild>
<Link href={`/appointments/${appointment?._id}`}>Show Details</Link>
</Button>
<Button variant="outline" asChild>
</ButtonV2>
<ButtonV2 variant="gooeyRight" asChild>
<Link href="/new-appointment">Book Another Appointment</Link>
</Button>
</ButtonV2>
</CardFooter>
)}
</Card>
Expand Down
22 changes: 12 additions & 10 deletions server/src/domain/entities/IVideoChatSection.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PaginatedResult } from "../../../types";
import IAppointment, { AppointmentStatus, IExtendedAppointment } from "../../entities/IAppointment";

export default interface IAppointmentRepository {
create(appointment: IAppointment): Promise<string>;
create(appointment: IAppointment): Promise<IAppointment>;
update(appointment: IAppointment): Promise<IAppointment | null>;
updateManyBySlotIdsNotInStatuses(slotIds: string[], fields: IAppointment, notInStatuses:AppointmentStatus[]): Promise<IAppointment[] | null>;
findByDateAndSlot(appointmentDate: string, slotId: string): Promise<IAppointment | null>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PaginatedResult } from "../../../types";

export default interface IDoctorRepository {
findByEmail(email: string): Promise<IDoctor | null>;
findByID(id: string | Types.ObjectId): Promise<IDoctor | null>;
findById(id: string | Types.ObjectId): Promise<IDoctor | null>;
findByEmailWithCredentials(email: string): Promise<IDoctor | null>;
create(doctor: IDoctor): Promise<string>;
update(doctor: IDoctor): Promise<IDoctor | null>;
Expand Down
2 changes: 2 additions & 0 deletions server/src/infrastructure/model/VideoSectionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const videoSectionSchema = new Schema<IVideoSection>({
enum: Object.values(VideoSectionStatus),
default: VideoSectionStatus.PENDING
},
doctorProfile: { type: String, required: true },
patientProfile: { type: String, required: true },
})

const VideoSectionModel = model<IVideoSection>('VideoSection', videoSectionSchema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { getPaginatedResult } from "./getPaginatedResult";
export default class AppointmentRepository implements IAppointmentRepository {
model = AppointmentModel;

async create(appointment: IAppointment): Promise<string> {
return (await this.model.create(appointment))._id;
async create(appointment: IAppointment): Promise<IAppointment> {
return (await this.model.create(appointment));
}
async update(appointment: IAppointment): Promise<IAppointment | null> {
return await this.model.findByIdAndUpdate(appointment._id, appointment, { new: true });
Expand Down
2 changes: 1 addition & 1 deletion server/src/infrastructure/repositories/DoctorRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class DoctorRepository implements IDoctorRepository {
async findByEmail(email: string): Promise<IDoctor | null> {
return await this.model.findOne({ email }).select(["-token", "-password"]);
}
async findByID(id: string | Types.ObjectId): Promise<IDoctor | null> {
async findById(id: string | Types.ObjectId): Promise<IDoctor | null> {
return await this.model.findById(id).select(["-token", "-password"]);
}
async findByEmailWithCredentials(email: string): Promise<IDoctor | null> {
Expand Down
13 changes: 11 additions & 2 deletions server/src/presentation/routers/appointment/AppointmentRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand Down
59 changes: 50 additions & 9 deletions server/src/use_case/appointment/CreateAppointmentUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,14 +23,22 @@ 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;
}

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);

Expand Down Expand Up @@ -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,
Expand All @@ -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<void> {
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<void> {
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<IPayment | null> {

private async verifyPaymentIntent(id: string, transactionId: string): Promise<IPayment | null> {
const payment = await this.paymentRepository.findById(id);

if (!payment) {
Expand All @@ -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!);
Expand Down
2 changes: 1 addition & 1 deletion server/src/use_case/chat/CreateChatUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions server/src/use_case/doctor/AuthenticationUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -138,7 +138,7 @@ export default class AuthenticationUseCase {

async updateProfileImage(key: string, id: string): Promise<void> {
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);

Expand All @@ -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);
Expand Down
21 changes: 4 additions & 17 deletions server/src/use_case/slot/CreateSlotUseCase.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
}
}
8 changes: 8 additions & 0 deletions server/src/utils/date-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { parse, format, addMinutes, addHours } from "date-fns";

export {
parse,
format,
addMinutes,
addHours
}

0 comments on commit b62dd03

Please sign in to comment.