Skip to content

Commit

Permalink
razorpay payment service added
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 16, 2024
1 parent 524a50a commit aabb4b8
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 42 deletions.
5 changes: 3 additions & 2 deletions client/components/forms/patient/AppointmentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useCreateAppointment, useGetDoctorsList } from "@/lib/hooks/appointment/useAppointment";
import { useCompletePaymentAppointment, 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";
Expand All @@ -25,6 +25,7 @@ const AppointmentForm = () => {
const { data: doctorsData, isLoading: isDoctorsLoading } = useGetDoctorsList();
const [isDoctorSelected, setIsDoctorSelected] = useState(false);
const { mutate: createAppointment, isPending } = useCreateAppointment();
const {mutate:completePayment} = useCompletePaymentAppointment()
const query = useQueryClient();

const form = useForm<z.infer<typeof appointmentFormValidation>>({
Expand Down Expand Up @@ -76,7 +77,7 @@ const AppointmentForm = () => {
},
},
{
onSuccess() {
onSuccess: async({paymentSessionId})=> {
toast({
title: "Appointment Created",
description: "We will notify you once the doctor approves your appointment",
Expand Down
5 changes: 5 additions & 0 deletions client/lib/api/appointment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ export const getDoctorsList = async () => {
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);
return response.data;
}
17 changes: 15 additions & 2 deletions client/lib/hooks/appointment/useAppointment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { createAppointment, getDoctorsList } from "@/lib/api/appointment"
import { completePayment, createAppointment, getDoctorsList } from "@/lib/api/appointment"
import IAppointment, { ErrorResponse, IDoctor, 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<PaginatedResult<IDoctor>, AxiosError<ErrorResponse>>({
queryFn: () => getDoctorsList(),
Expand All @@ -12,10 +16,19 @@ export const useGetDoctorsList = () => {
};

export const useCreateAppointment = ()=>{
return useMutation<MessageResponse,AxiosError<ErrorResponse>,{appointment:IAppointment}>({
return useMutation<MessageWithSessionId,AxiosError<ErrorResponse>,{appointment:IAppointment}>({
mutationFn:({appointment})=>createAppointment(appointment),
onError:(error)=>{
console.log("Error in creating appointment",error);
}
})
}

export const useCompletePaymentAppointment = ()=>{
return useMutation<MessageWithSessionId,AxiosError<ErrorResponse>,{data:any}>({
mutationFn:({data})=>completePayment(data),
onError:(error)=>{
console.log("Error in completing payment ",error);
}
})
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
},
"devDependencies": {
"concurrently": "^8.2.2"
},
"dependencies": {
"razorpay": "^2.9.4"
}
}
}
1 change: 1 addition & 0 deletions server/src/domain/entities/IAppointment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export default interface IAppointment {
readonly appointmentDate?: string;
readonly reason?: string;
readonly notes?: string;
readonly paymentId?: string;
status?: AppointmentStatus;
}
18 changes: 18 additions & 0 deletions server/src/domain/entities/IPayment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export enum PaymentStatus {
PENDING = "PENDING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
}

export default interface IPayment {
_id?: string;
orderId: string;
paymentId?: string;
appointmentId?: string;
amount?: number;
currency?: string;
status?: PaymentStatus;
createdAt?: Date;
updatedAt?: Date;
razorpaySignature?: string;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import IAppointment, { AppointmentStatus } from "../../entities/IAppointment";

export default interface IAppointmentRepository {
create(appointment: IAppointment): Promise<void>;
create(appointment: IAppointment): Promise<string>;
update(appointment: IAppointment): Promise<void>;
updateStatusMany(appointmentIds: string[], status: AppointmentStatus): Promise<void>;
updateManyBySlotIds(slotIds: string[], fields: IAppointment): Promise<void>;
findOneBySlotId(slotId: string): Promise<IAppointment | null>;
findByDateAndSlot(appointmentDate: string, slotId:string): Promise<IAppointment | null>;
findManyByDateAndDoctorId(appointmentDate:string,doctorId:string):Promise<IAppointment[] | null>;
updateAppointmentStatusToConfirmed(appointmentId:string):Promise<void>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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>;
update(payment: IPayment): Promise<void>;
}
12 changes: 12 additions & 0 deletions server/src/domain/interface/services/IPaymentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default interface IPaymentService {
createOrder(amount: number, currency: string, receipt: string): Promise<RazorpayOrder>;
verifyPaymentSignature(signature: string, orderId: string, paymentId: string): Promise<void>;
}

export interface RazorpayOrder {
id: string;
amount: string | number;
currency: string;
receipt?: string;
status: string;
}
7 changes: 6 additions & 1 deletion server/src/infrastructure/database/AppointmentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const appointmentSchema = new Schema<IAppointment>(
},
reason: {
type: String,
default: null,
required:true
},
notes: {
type: String,
Expand All @@ -44,6 +44,11 @@ const appointmentSchema = new Schema<IAppointment>(
default: AppointmentStatus.PENDING,
required: true,
},
paymentId:{
type:String,
required:true,
default:null
}
},
{
timestamps: true,
Expand Down
47 changes: 47 additions & 0 deletions server/src/infrastructure/database/PaymentModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { model, Schema } from 'mongoose';
import IPayment, { PaymentStatus } from '../../domain/entities/IPayment';

const paymentSchema = new Schema<IPayment>(
{
orderId: {
type: String,
required: true,
},
paymentId: {
type: String,
default: null,
},
appointmentId: {
type: Schema.Types.ObjectId,
ref: 'Appointment',
required: true,
index: true,
},
amount: {
type: Number,
required: true,
},
currency: {
type: String,
required: true,
},
status: {
type: String,
enum: Object.values(PaymentStatus),
default: PaymentStatus.PENDING,
required: true,
},
razorpaySignature: {
type: String,
default: null,
},
},
{
timestamps: true,
versionKey: false,
minimize: false,
}
);
const PaymentModel = model<IPayment>('Payment', paymentSchema);

export default PaymentModel;
12 changes: 8 additions & 4 deletions server/src/infrastructure/repositories/AppointmentRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import AppointmentModel from "../database/AppointmentModel";

export default class AppointmentRepository implements IAppointmentRepository {
model = AppointmentModel
async create(appointment: IAppointment): Promise<void> {
await this.model.create(appointment)
async create(appointment: IAppointment): Promise<string> {
return (await this.model.create(appointment))._id
}
async update(appointment: IAppointment): Promise<void> {
await this.model.findByIdAndUpdate(appointment._id, appointment, { new: true })
Expand All @@ -14,9 +14,9 @@ export default class AppointmentRepository implements IAppointmentRepository {
async findOneBySlotId(slotId: string): Promise<IAppointment | null> {
return await this.model.findOne({ slotId });
}

async findManyByDateAndDoctorId(appointmentDate: string, doctorId: string): Promise<IAppointment[] | null> {
return await this.model.find({appointmentDate,doctorId});
return await this.model.find({ appointmentDate, doctorId });
}
async findByDateAndSlot(appointmentDate: string, slotId: string): Promise<IAppointment | null> {
return await this.model.findOne({ appointmentDate, slotId })
Expand All @@ -30,4 +30,8 @@ export default class AppointmentRepository implements IAppointmentRepository {
await this.model.updateMany({ slotId: { $in: slotIds } }, fields);
}

async updateAppointmentStatusToConfirmed(appointmentId: string): Promise<void> {
await this.model.findByIdAndUpdate(appointmentId, { status: AppointmentStatus.PENDING });
}

}
26 changes: 26 additions & 0 deletions server/src/infrastructure/repositories/PaymentRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// src/repositories/PaymentRepository.ts

import IPayment from "../../domain/entities/IPayment";
import IPaymentRepository from "../../domain/interface/repositories/IPaymentRepository";
import PaymentModel from "../database/PaymentModel";

export default class PaymentRepository implements IPaymentRepository {
model = PaymentModel;

async create(payment: IPayment): Promise<IPayment> {
return await this.model.create(payment);
}

async findById(id: string): Promise<IPayment | null> {
return await this.model.findById(id).exec();
}

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

async update(payment: IPayment): Promise<void> {
const { _id, ...updateData } = payment;
await this.model.findByIdAndUpdate(_id, updateData, { new: true }).exec();
}
}
52 changes: 52 additions & 0 deletions server/src/infrastructure/services/RazorPayService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Razorpay from 'razorpay';
import IPaymentService, { RazorpayOrder } from '../../domain/interface/services/IPaymentService';
import { StatusCode } from '../../types';
import CustomError from '../../domain/entities/CustomError'
import * as crypto from 'crypto';
import logger from '../../utils/logger';


export default class RazorPayService implements IPaymentService {
private razorpay: Razorpay;

constructor() {
this.razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KET_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
}

async createOrder(amount: number, currency: string, receipt: string): Promise<RazorpayOrder> {
const options = {
amount: amount * 100,
currency,
receipt,
payment_capture: 1,
};

try {
const order = await this.razorpay.orders.create(options);
return {
...order,
amount: typeof order.amount === 'string' ? parseInt(order.amount, 10) : order.amount,
};
} catch (error) {
logger.error(error)
throw new CustomError('Error creating Razorpay order', StatusCode.PaymentError);
}
}

async verifyPaymentSignature(signature: string, orderId: string, paymentId: string): Promise<void> {
try {
const generatedSignature = crypto.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET!)
.update(orderId + '|' + paymentId)
.digest('hex');

if (generatedSignature !== signature) {
throw new CustomError('Invalid payment signature', StatusCode.PaymentError);
}
} catch (error) {
throw new CustomError('Error verifying payment signature', StatusCode.PaymentError);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ export default class AppointmentController {
try {
const { appointment } = req.body;
const patientId = req.patient?.id;
await this.appointmentUseCase.create(appointment, patientId!);
res.status(StatusCode.Success).json({ message: "Appointment created successfully" });
const { appointmentId, orderId } = await this.appointmentUseCase.createAppointment(appointment, patientId!);
res.status(StatusCode.Success).json({ orderId, appointmentId });
} catch (error: any) {
next(error);
}
}

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)
res.status(StatusCode.Success).json({ message: "Payment Verification Completed" });
} catch (error) {
next(error)
}
}

}
11 changes: 8 additions & 3 deletions server/src/presentation/routers/appointment/AppointmentRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import AppointmentController from '../../controllers/appointment/AppointmentCont
import PatientAuthMiddleware from '../../middlewares/PatientAuthMiddleware';
import JWTService from '../../../infrastructure/services/JWTService';
import JoiService from '../../../infrastructure/services/JoiService';
import RazorPayService from '../../../infrastructure/services/RazorPayService';
import PaymentRepository from '../../../infrastructure/repositories/PaymentRepository';

const router = express.Router();


const appointmentRepository = new AppointmentRepository();
const slotRepository = new SlotRepository();
const tokenService = new JWTService()
const validatorService = new JoiService()
const tokenService = new JWTService();
const validatorService = new JoiService();
const paymentService = new RazorPayService();
const paymentRepository = new PaymentRepository()

const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository, validatorService);
const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository, validatorService, paymentService, paymentRepository);

const appointmentController = new AppointmentController(appointmentUseCase);

const authorizePatient = new PatientAuthMiddleware(tokenService);

router.post('/', authorizePatient.exec.bind(authorizePatient), appointmentController.create.bind(appointmentController));
router.put('/payment', authorizePatient.exec.bind(authorizePatient), appointmentController.completePayment.bind(appointmentController))

export default router;
1 change: 1 addition & 0 deletions server/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum StatusCode {
NoContent = 204,
BadRequest = 400,
Unauthorized = 401,
PaymentError=402,
Forbidden = 403,
NotFound = 404,
Conflict = 409,
Expand Down
Loading

1 comment on commit aabb4b8

@vercel
Copy link

@vercel vercel bot commented on aabb4b8 Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.