Skip to content

Commit

Permalink
📦 refund on appointment cancellation added
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Oct 6, 2024
1 parent 70a7ee8 commit 8ee28c7
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 60 deletions.
2 changes: 1 addition & 1 deletion client/app/(patient)/appointments/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function AppointmentDetailsPage() {
onSuccess: () => {
toast({
title: "Appointment Cancelled",
description: "Your appointment has been successfully cancelled.",
description: "Your appointment has been successfully cancelled. Refund will be getting to your account in 10 working days 💸.",
})
refetch()
setCancelModelOpen(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import IPayment from "../../entities/IPayment";
export default interface IPaymentRepository {
create(payment: IPayment): Promise<IPayment>;
findById(id: string): Promise<IPayment | null>;
findByOrderId(orderId: string): Promise<IPayment | null>;
findByAppointmentId(appointmentId: string): Promise<IPayment | null>;
update(payment: IPayment): Promise<void>;
}
2 changes: 1 addition & 1 deletion server/src/domain/interface/services/IPaymentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default interface IPaymentService {
metadata?: Record<string, any>
): Promise<{ id: string; url: string }>;
retrievePaymentIntent(paymentIntentId: string): Promise<any>;
handleWebhookEvent(body: Buffer, signature: string): Promise<{ event: any; transactionId: string }>;
handleWebhookEvent(body: Buffer, signature: string): Promise<{ event: any; transactionId: string,type:"charge"|"paymentSuccess"|"" }>;
refundPayment(paymentIntentId: string, amount?: number, reason?: string): Promise<any>
}
4 changes: 2 additions & 2 deletions server/src/infrastructure/repositories/PaymentRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default class PaymentRepository implements IPaymentRepository {
return await this.model.findById(id).exec();
}

async findByOrderId(orderId: string): Promise<IPayment | null> {
return await this.model.findOne({ orderId }).exec();
async findByAppointmentId(appointmentId: string): Promise<IPayment | null> {
return await this.model.findOne({ appointmentId }).lean(true);
}

async update(payment: IPayment): Promise<void> {
Expand Down
24 changes: 14 additions & 10 deletions server/src/infrastructure/services/StripeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,23 @@ class StripePaymentService implements IPaymentService {
}
}

async handleWebhookEvent(body: Buffer, signature: string): Promise<{ event: any; transactionId: string }> {
async handleWebhookEvent(body: Buffer, signature: string): Promise<{ event: any; transactionId: string, type: "charge" | "paymentSuccess" | "" }> {
try {
const event = stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET!);
let res: { event: any; transactionId: string } = { event: null, transactionId: "" };
let res: { event: any; transactionId: string, type: "charge" | "paymentSuccess" | "" } = { event: null, transactionId: "", type: "" };
switch (event.type) {
case "payment_intent.succeeded":
const paymentIntent = event.data.object as Stripe.PaymentIntent;
res = { event, transactionId: paymentIntent.id };
res = { event, transactionId: paymentIntent.id, type: "paymentSuccess" };
break;

case "payment_intent.payment_failed":
throw new CustomError("Payment failed", StatusCode.PaymentError);

case "charge.succeeded":
const charge = event.data.object as Stripe.Charge
res = { event, transactionId: charge.id, type: "charge" }

default:
break;
}
Expand All @@ -83,17 +87,17 @@ class StripePaymentService implements IPaymentService {
}
}

async refundPayment(paymentIntentId: string, amount?: number): Promise<Stripe.Refund | any> {
async refundPayment(paymentId: string, amount?: number): Promise<any> {
try {
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);

const refund = await stripe.refunds.create({
charge: paymentIntent.id,
await stripe.refunds.create({
charge: paymentId,
amount: amount ? amount * 100 : undefined,
reason: "requested_by_customer",
});
return refund;
} catch (error) {
} catch (error: any) {
if (error.raw.code === 'charge_already_refunded') {
return null
}
logger.error(error);
return null
}
Expand Down
14 changes: 8 additions & 6 deletions server/src/presentation/routers/appointment/AppointmentRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const notificationRepository = new NotificationRepository();
const videoSectionRepository = new VideoSectionRepository();
const patientRepository = new PatientRepository();
const doctorRepository = new DoctorRepository();
const prescriptionRepository = new PrescriptionRepository()
const prescriptionRepository = new PrescriptionRepository();

const createAppointmentUseCase = new CreateAppointmentUseCase(
appointmentRepository,
Expand All @@ -45,13 +45,15 @@ const createAppointmentUseCase = new CreateAppointmentUseCase(
patientRepository,
uuIdService
);
const getAppointmentUseCase = new GetAppointmentUseCase(appointmentRepository, validatorService, paymentRepository, prescriptionRepository);
const updateAppointmentUseCase = new UpdateAppointmentUseCase(appointmentRepository, validatorService, notificationRepository, videoSectionRepository);
const getAppointmentUseCase = new GetAppointmentUseCase(
appointmentRepository, validatorService, paymentRepository, prescriptionRepository
);
const updateAppointmentUseCase = new UpdateAppointmentUseCase(
appointmentRepository, validatorService, notificationRepository, videoSectionRepository, paymentService, paymentRepository
);

const appointmentController = new AppointmentController(
createAppointmentUseCase,
getAppointmentUseCase,
updateAppointmentUseCase
createAppointmentUseCase, getAppointmentUseCase, updateAppointmentUseCase
);
const authorizePatient = new PatientAuthMiddleware(tokenService);
const authorizeDoctor = new DoctorAuthMiddleware(tokenService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ const doctorRepository = new DoctorRepository();
const otpRepository = new OtpRepository();

const authUseCase = new AuthenticationUseCase(
doctorRepository,
passwordService,
tokenService,
emailService,
otpRepository,
cloudService,
validatorService
doctorRepository, passwordService, tokenService, emailService, otpRepository, cloudService, validatorService
);
const authDoctorController = new AuthDoctorController(authUseCase);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,9 @@ const patientRepository = new PatientRepository();
const otpRepository = new OtpRepository();

const authPatientUseCase = new AuthPatientUseCase(
patientRepository,
passwordService,
emailService,
otpRepository,
tokenService,
validatorService
patientRepository, passwordService, emailService, otpRepository, tokenService, validatorService
);

const authenticationController = new AuthenticationController(authPatientUseCase);

router.post("/", authenticationController.register.bind(authenticationController));
Expand Down
4 changes: 3 additions & 1 deletion server/src/presentation/routers/patient/PatientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const videoSectionRepository = new VideoSectionRepository();
const s3StorageService = new S3StorageService();
const validatorService = new JoiService();

const patientUseCase = new PatientUseCase(patientRepository, s3StorageService, validatorService, chatRepository, videoSectionRepository);
const patientUseCase = new PatientUseCase(
patientRepository, s3StorageService, validatorService, chatRepository, videoSectionRepository
);
const patientController = new PatientController(patientUseCase);

router.get("/profile", patientController.getProfile.bind(patientController));
Expand Down
6 changes: 5 additions & 1 deletion server/src/presentation/socket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import JoiService from "../../infrastructure/services/JoiService";
import JWTService from "../../infrastructure/services/JWTService";
import GetChatUseCase from "../../use_case/chat/GetChatUseCase";
import SocketServer from "./SocketServer";
import StripePaymentService from "../../infrastructure/services/StripeService";
import PaymentRepository from "../../infrastructure/repositories/PaymentRepository";

const tokenService = new JWTService();
const validationService = new JoiService();
const paymentService = new StripePaymentService()

const appointmentRepository = new AppointmentRepository();
const videoRepository = new VideoSectionRepository();
Expand All @@ -27,9 +30,10 @@ const patientRepository = new PatientRepository();
const messageRepository = new MessageRepository();
const chatRepository = new ChatRepository();
const doctorRepository = new DoctorRepository();
const paymentRepository = new PaymentRepository()

const updateAppointmentUseCase = new UpdateAppointmentUseCase(
appointmentRepository, validationService, notificationRepository, videoRepository
appointmentRepository, validationService, notificationRepository, videoRepository, paymentService, paymentRepository
);
const createChatUseCase = new CreateChatUseCase(
messageRepository, chatRepository, validationService, patientRepository, doctorRepository
Expand Down
25 changes: 15 additions & 10 deletions server/src/use_case/appointment/CreateAppointmentUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import IAppointment, { AppointmentStatus, AppointmentType } from "../../domain/entities/IAppointment";
import IVideoSectionRepository from "../../domain/interface/repositories/IVideoSectionRepository";
import IVideoSectionRepository from "../../domain/interface/repositories/IVideoSectionRepository";
import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository";
import IPatientRepository from "../../domain/interface/repositories/IPatientRepository";
import IPaymentRepository from "../../domain/interface/repositories/IPaymentRepository";
Expand Down Expand Up @@ -93,15 +93,15 @@ export default class AppointmentUseCase {
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");

const randomId = this.uuIdService.generate()

await this.videoSectionRepository.create({
appointmentId: appointment._id!,
patientName: patient.name,
Expand All @@ -121,7 +121,7 @@ export default class AppointmentUseCase {

async handleStripeWebhook(body: Buffer, signature: string): Promise<void> {
const result = await this.paymentService.handleWebhookEvent(body, signature);
const { event, transactionId } = result;
const { event, transactionId, type } = result;

if (!event || !event.data || !event.data.object) {
return;
Expand All @@ -132,22 +132,27 @@ export default class AppointmentUseCase {
return;
}

await this.verifyPaymentIntent(paymentIntentMetadata.paymentId, transactionId);
await this.verifyPaymentIntent(paymentIntentMetadata.paymentId, transactionId, type);
}


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

if (!payment) {
return null;
}

await this.paymentRepository.update({
const fields:any = {
_id: payment._id,
status: PaymentStatus.COMPLETED,
paymentId: transactionId
});
}

if(type==="charge"){
fields.paymentId = transactionId;
}

await this.paymentRepository.update(fields);

await this.appointmentRepository.updateAppointmentStatusToConfirmed(payment.appointmentId!);

Expand Down
47 changes: 33 additions & 14 deletions server/src/use_case/appointment/UpdateAppointmentUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository";
import IVideoSectionRepository from "../../domain/interface/repositories/IVideoSectionRepository";
import INotificationRepository from "../../domain/interface/repositories/INotificationRepository";
import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository";
import IPaymentRepository from "../../domain/interface/repositories/IPaymentRepository";
import IValidatorService from "../../domain/interface/services/IValidatorService";
import { AppointmentStatus } from "../../domain/entities/IAppointment";
import { NotificationTypes } from "../../domain/entities/INotification";
import IPaymentService from "../../domain/interface/services/IPaymentService";
import { VideoSectionStatus } from "../../domain/entities/IVideoChatSection";
import { NotificationTypes } from "../../domain/entities/INotification";
import { AppointmentStatus } from "../../domain/entities/IAppointment";

export default class UpdateAppointmentUseCase {
constructor(
private appointmentRepository: IAppointmentRepository,
private validatorService: IValidatorService,
private notificationRepository: INotificationRepository,
private videoSectionRepository: IVideoSectionRepository
private videoSectionRepository: IVideoSectionRepository,
private paymentService: IPaymentService,
private paymentRepository: IPaymentRepository
) { }

// By Doctor
Expand All @@ -20,15 +24,9 @@ export default class UpdateAppointmentUseCase {
this.validatorService.validateIdFormat(appointmentId);
this.validatorService.validateEnum(status, Object.values(AppointmentStatus));
const appointment = await this.appointmentRepository.update(appointmentId, { status });
// notify patient

if (status === AppointmentStatus.CANCELLED && appointment) {
await this.notificationRepository.create({
appointmentId,
message: `🚫 Your appointment has been canceled. If you have any questions, please contact your doctor.`,
patientId: appointment.patientId,
type: NotificationTypes.APPOINTMENT_CANCELED
});
await this.videoSectionRepository.findByAppointmentIdAndUpdate(appointmentId, { status: VideoSectionStatus.CANCELLED });
await this.handleCancellation(appointmentId, appointment);
}

if (status === AppointmentStatus.CONFIRMED && appointment) {
Expand All @@ -39,16 +37,36 @@ export default class UpdateAppointmentUseCase {
type: NotificationTypes.APPOINTMENT_CONFIRMED
});
}

}

// By Patient
async updateStatusAndNote(appointmentId: string, status: AppointmentStatus, notes: string): Promise<void> {
this.validatorService.validateRequiredFields({ appointmentId, status, notes });
this.validatorService.validateEnum(status, Object.values(AppointmentStatus));
const appointment = await this.appointmentRepository.update(appointmentId, { status, notes });
// notify doctor

if (status === AppointmentStatus.CANCELLED && appointment) {
await this.handleCancellation(appointmentId, appointment, true);
}
}

// Handle appointment cancellation
private async handleCancellation(appointmentId: string, appointment: any, notifyDoctor: boolean = false) {
await this.notificationRepository.create({
appointmentId,
message: `🚫 Your appointment has been canceled. Your refund will be credited to your account in 10 working days. If you have any questions, please contact your doctor.`,
patientId: appointment.patientId,
type: NotificationTypes.APPOINTMENT_CANCELED
});

await this.videoSectionRepository.findByAppointmentIdAndUpdate(appointmentId, { status: VideoSectionStatus.CANCELLED });

const payment = await this.paymentRepository.findByAppointmentId(appointment._id!);
if (payment) {
await this.paymentService.refundPayment(payment.paymentId!, payment.amount);
}

if (notifyDoctor) {
await this.notificationRepository.create({
appointmentId,
message: `🚫 The appointment for patient has been canceled. The slot is now available for other patients.`,
Expand All @@ -57,6 +75,7 @@ export default class UpdateAppointmentUseCase {
});
}
}

async updateCompleteSection(roomId: string, doctorId: string) {
const room = await this.videoSectionRepository.findByRoomId(roomId);
if (!room) {
Expand Down

0 comments on commit 8ee28c7

Please sign in to comment.