diff --git a/client/components/forms/patient/AppointmentForm.tsx b/client/components/forms/patient/AppointmentForm.tsx index 2042ee45..53a7b5f3 100644 --- a/client/components/forms/patient/AppointmentForm.tsx +++ b/client/components/forms/patient/AppointmentForm.tsx @@ -1,4 +1,4 @@ -'use client' +'use client'; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -11,7 +11,7 @@ import { SelectItem } from "@/components/ui/select"; import Image from "next/image"; import { AppointmentTypes } from "@/constants"; import { FormFieldType } from "@/types/fromTypes"; -import { useCompletePaymentAppointment, useCreateAppointment, useGetDoctorsList } from "@/lib/hooks/appointment/useAppointment"; +import { useVerifyPaymentAppointment, useCreateAppointment, useGetDoctorsList } from "@/lib/hooks/appointment/useAppointment"; import { useGetSlotsOfDoctor } from "@/lib/hooks/slots/useSlot"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -20,12 +20,20 @@ import { FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/for import { toast } from "@/components/ui/use-toast"; import { AppointmentType } from "@/types"; import { useQueryClient } from "@tanstack/react-query"; +import loadRazorpayScript from '@/lib/utils/loadRazorpayScript' + +// Declare Razorpay in the global scope +declare global { + interface Window { + Razorpay: any; + } +} const AppointmentForm = () => { const { data: doctorsData, isLoading: isDoctorsLoading } = useGetDoctorsList(); + const { mutate: verifyPayment } = useVerifyPaymentAppointment(); const [isDoctorSelected, setIsDoctorSelected] = useState(false); const { mutate: createAppointment, isPending } = useCreateAppointment(); - const {mutate:completePayment} = useCompletePaymentAppointment() const query = useQueryClient(); const form = useForm>({ @@ -77,15 +85,60 @@ const AppointmentForm = () => { }, }, { - onSuccess: async({paymentSessionId})=> { + onSuccess: async ({ appointmentId, orderId, patient }) => { toast({ title: "Appointment Created", - description: "We will notify you once the doctor approves your appointment", + description: "Redirecting to payment...", variant: "success", }); - query.invalidateQueries({ - queryKey: ["doctorSlots", slotFilter.doctorId, slotFilter.date instanceof Date ? slotFilter.date.toISOString() : ""] - }) + + // Load Razorpay SDK + const isRazorpayLoaded = await loadRazorpayScript(); + if (!isRazorpayLoaded) { + toast({ + title: "Payment Error", + description: "Razorpay SDK failed to load. Please try again.", + variant: "destructive", + }); + return; + } + + const paymentOptions = { + key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, + amount: 300 * 100, + currency: "INR", + name: "Your App", + description: "Appointment Payment", + order_id: orderId, + handler: async function (response: { razorpay_order_id: string, razorpay_payment_id: string, razorpay_signature: string }) { + verifyPayment({ + data: { + appointmentId, + paymentData: { + razorpay_order_id: response.razorpay_order_id, + razorpay_payment_id: response.razorpay_payment_id, + razorpay_signature: response.razorpay_signature, + } + } + }); + toast({ + title: "Payment Success", + description: "Your payment was successful and the appointment is confirmed.", + variant: "success", + }); + }, + prefill: { + name: patient.name, + email: patient.email, + contact: patient.phone, + }, + theme: { + color: "#3399cc", + }, + }; + + const paymentObject = new window.Razorpay(paymentOptions); + paymentObject.open(); }, onError(error) { const message = diff --git a/client/lib/api/appointment/index.ts b/client/lib/api/appointment/index.ts index 8ba0f723..a4596835 100644 --- a/client/lib/api/appointment/index.ts +++ b/client/lib/api/appointment/index.ts @@ -72,12 +72,17 @@ export const getDoctorsList = async () => { return response.data; }; -export const createAppointment = async (appointment:IAppointment)=>{ - const response = await axiosInstance.post('/',{appointment}); +export const createAppointment = async (appointment: IAppointment) => { + const response = await axiosInstance.post('/', { appointment }); return response.data; } -export const completePayment = async (data:any)=>{ - const response = await axiosInstance.put('/',data); +export const verifyPayment = async ({ appointmentId, paymentData }: verifyPaymentProps) => { + const response = await axiosInstance.post('/verify-payment', { paymentData, appointmentId }); return response.data; +} + +export type verifyPaymentProps = { + paymentData: { razorpay_order_id: string; razorpay_payment_id: string; razorpay_signature: string }; + appointmentId: string } \ No newline at end of file diff --git a/client/lib/hooks/appointment/useAppointment.ts b/client/lib/hooks/appointment/useAppointment.ts index 7c61c0a5..5f52c78a 100644 --- a/client/lib/hooks/appointment/useAppointment.ts +++ b/client/lib/hooks/appointment/useAppointment.ts @@ -1,12 +1,8 @@ -import { completePayment, createAppointment, getDoctorsList } from "@/lib/api/appointment" -import IAppointment, { ErrorResponse, IDoctor, MessageResponse, PaginatedResult } from "@/types" +import { verifyPayment, createAppointment, getDoctorsList, verifyPaymentProps } from "@/lib/api/appointment" +import IAppointment, { ErrorResponse, IDoctor, IPatient, MessageResponse, PaginatedResult } from "@/types" import { useMutation, useQuery } from "@tanstack/react-query" import { AxiosError } from "axios" -interface MessageWithSessionId extends MessageResponse{ - paymentSessionId:string -} - export const useGetDoctorsList = () => { return useQuery, AxiosError>({ queryFn: () => getDoctorsList(), @@ -15,20 +11,20 @@ export const useGetDoctorsList = () => { }); }; -export const useCreateAppointment = ()=>{ - return useMutation,{appointment:IAppointment}>({ - mutationFn:({appointment})=>createAppointment(appointment), - onError:(error)=>{ - console.log("Error in creating appointment",error); +export const useCreateAppointment = () => { + return useMutation<{ orderId: string, appointmentId: string, patient:IPatient }, AxiosError, { appointment: IAppointment }>({ + mutationFn: ({ appointment }) => createAppointment(appointment), + onError: (error) => { + console.log("Error in creating appointment", error); } }) } -export const useCompletePaymentAppointment = ()=>{ - return useMutation,{data:any}>({ - mutationFn:({data})=>completePayment(data), - onError:(error)=>{ - console.log("Error in completing payment ",error); +export const useVerifyPaymentAppointment = () => { + return useMutation, { data: verifyPaymentProps }>({ + mutationFn: ({ data }) => verifyPayment(data), + onError: (error) => { + console.log("Error in Verifying payment ", error); } }) } \ No newline at end of file diff --git a/client/lib/utils/loadRazorpayScript.ts b/client/lib/utils/loadRazorpayScript.ts new file mode 100644 index 00000000..6f368a14 --- /dev/null +++ b/client/lib/utils/loadRazorpayScript.ts @@ -0,0 +1,11 @@ +const loadRazorpayScript = (): Promise => { + return new Promise((resolve) => { + const script = document.createElement("script"); + script.src = "https://checkout.razorpay.com/v1/checkout.js"; + script.onload = () => resolve(true); + script.onerror = () => resolve(false); + document.body.appendChild(script); + }); + }; + + export default loadRazorpayScript \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 39ff47fa..15ce230d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -37,6 +37,7 @@ "next": "^14.2.7", "next-themes": "^0.3.0", "otp-timer-ts": "^3.0.1", + "razorpay": "^2.9.4", "react": "^18", "react-datepicker": "^7.3.0", "react-dom": "^18", @@ -6726,6 +6727,15 @@ ], "license": "MIT" }, + "node_modules/razorpay": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.4.tgz", + "integrity": "sha512-CvOitdgM5HNr+zl174fHpZoJKVFYEfPoGx798kX+kg3haGDD3xinzxTzRUIOzLnL1/F4e7mUoIaGNn0h1E929Q==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", diff --git a/client/package.json b/client/package.json index 735cf212..cd5e0236 100644 --- a/client/package.json +++ b/client/package.json @@ -40,6 +40,7 @@ "next": "^14.2.7", "next-themes": "^0.3.0", "otp-timer-ts": "^3.0.1", + "razorpay": "^2.9.4", "react": "^18", "react-datepicker": "^7.3.0", "react-dom": "^18", diff --git a/server/src/presentation/controllers/appointment/AppointmentControllers.ts b/server/src/presentation/controllers/appointment/AppointmentControllers.ts index 2e0d3181..accbb5a0 100644 --- a/server/src/presentation/controllers/appointment/AppointmentControllers.ts +++ b/server/src/presentation/controllers/appointment/AppointmentControllers.ts @@ -11,8 +11,8 @@ export default class AppointmentController { try { const { appointment } = req.body; const patientId = req.patient?.id; - const { appointmentId, orderId } = await this.appointmentUseCase.createAppointment(appointment, patientId!); - res.status(StatusCode.Success).json({ orderId, appointmentId }); + const { appointmentId, orderId, patient } = await this.appointmentUseCase.createAppointment(appointment, patientId!); + res.status(StatusCode.Success).json({ orderId, appointmentId, patient }); } catch (error: any) { next(error); } @@ -20,8 +20,8 @@ export default class AppointmentController { async completePayment(req: CustomRequest, res: Response, next: NextFunction) { try { - const { razorpay_order_id, razorpay_payment_id, razorpay_signature, appointmentId } = req.body; - await this.appointmentUseCase.verifyPayment({ razorpay_order_id, razorpay_payment_id, razorpay_signature }, appointmentId) + const { appointmentId,paymentData } = req.body; + await this.appointmentUseCase.verifyPayment(paymentData, appointmentId) res.status(StatusCode.Success).json({ message: "Payment Verification Completed" }); } catch (error) { next(error) diff --git a/server/src/presentation/routers/appointment/AppointmentRoutes.ts b/server/src/presentation/routers/appointment/AppointmentRoutes.ts index 4632f901..b76a3ae5 100644 --- a/server/src/presentation/routers/appointment/AppointmentRoutes.ts +++ b/server/src/presentation/routers/appointment/AppointmentRoutes.ts @@ -8,6 +8,7 @@ import JWTService from '../../../infrastructure/services/JWTService'; import JoiService from '../../../infrastructure/services/JoiService'; import RazorPayService from '../../../infrastructure/services/RazorPayService'; import PaymentRepository from '../../../infrastructure/repositories/PaymentRepository'; +import PatientRepository from '../../../infrastructure/repositories/PatientRepository'; const router = express.Router(); @@ -17,9 +18,10 @@ const slotRepository = new SlotRepository(); const tokenService = new JWTService(); const validatorService = new JoiService(); const paymentService = new RazorPayService(); -const paymentRepository = new PaymentRepository() +const paymentRepository = new PaymentRepository(); +const patientRepository = new PatientRepository() -const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository, validatorService, paymentService, paymentRepository); +const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository, validatorService, paymentService, paymentRepository, patientRepository); const appointmentController = new AppointmentController(appointmentUseCase); diff --git a/server/src/use_case/appointment/AppointmentUseCase.ts b/server/src/use_case/appointment/AppointmentUseCase.ts index f854d145..15f99eae 100644 --- a/server/src/use_case/appointment/AppointmentUseCase.ts +++ b/server/src/use_case/appointment/AppointmentUseCase.ts @@ -7,6 +7,8 @@ import { StatusCode } from "../../types"; import IPaymentService from "../../domain/interface/services/IPaymentService"; import IPaymentRepository from "../../domain/interface/repositories/IPaymentRepository"; import { PaymentStatus } from "../../domain/entities/IPayment"; +import IPatientRepository from "../../domain/interface/repositories/IPatientRepository"; +import { IPatient } from "../../domain/entities/IPatient"; export default class AppointmentUseCase { bookingAmount: number; @@ -16,7 +18,8 @@ export default class AppointmentUseCase { private slotRepository: ISlotRepository, private validatorService: IValidatorService, private paymentService: IPaymentService, - private paymentRepository: IPaymentRepository + private paymentRepository: IPaymentRepository, + private patientRepository: IPatientRepository ) { this.bookingAmount = 300; } @@ -24,7 +27,7 @@ export default class AppointmentUseCase { async createAppointment( appointmentData: IAppointment, patientId: string - ): Promise<{ appointmentId: string, orderId: string }> { + ): Promise<{ appointmentId: string, orderId: string, patient: IPatient }> { this.validateAppointmentData(appointmentData, patientId); const slot = await this.slotRepository.findById(appointmentData.slotId!); @@ -48,7 +51,7 @@ export default class AppointmentUseCase { const razorpayOrder = await this.paymentService.createOrder(this.bookingAmount, 'INR', `receipt_${payment._id}`); - + const appointmentId = await this.appointmentRepository.create({ ...appointmentData, patientId, @@ -61,8 +64,10 @@ export default class AppointmentUseCase { orderId: razorpayOrder.id!, appointmentId }); - - return { orderId: razorpayOrder.id, appointmentId }; + + const patient = await this.patientRepository.findById(patientId)! + + return { orderId: razorpayOrder.id, appointmentId, patient: { email: patient?.email, name: patient?.name, phone: patient?.phone } }; } async verifyPayment(paymentData: { razorpay_order_id: string, razorpay_payment_id: string, razorpay_signature: string }, appointmentId: string): Promise {