Content ITV PRO
This is Itvedant Content department
Business Scenario
In this lab, you will prepare the Medicare application for final verification by setting up test doctor accounts and profiles, then testing the complete application through the React frontend.
Pre-Lab Preparation
Module:
1) React Basics & Backend Integration (Spring Boot)
Phase 15 — Final Data Setup + API & Frontend Testing
1. Add enough test data 2. Test the complete application from the frontend 3. Run a small final API/security test
Add test data first
1
Appointments Payments Prescriptions Patients
Doctors
Page 1 → 6 doctors Page 2 → remaining doctor
Doctor test accounts
2
| Doctor | Specialization | Experience | Fee | |
|---|---|---|---|---|
| Dr. Ananya Sharma | ananya.doctor@example.com | Cardiologist | 12 | 1800 |
| Dr. Rohit Mehta | rohit.doctor@example.com | Orthopedic | 8 | 1200 |
| Dr. Neha Patil | neha.doctor@example.com | Dentist | 6 | 800 |
| Dr. Arjun Rao | arjun.doctor@example.com | General Physician | 10 | 700 |
| Dr. Kavya Iyer | kavya.doctor@example.com | Cardiologist | 5 | 1400 |
| Dr. Sameer Khan | sameer.doctor@example.com | Orthopedic | 15 | 2000 |
| Dr. Priya Nair | priya.doctor@example.com | Dentist | 9 | 1000 |
Doctor@123
Easiest way to insert them
3
USE medicare_db;
INSERT IGNORE INTO users
(name, email, phone, role, password)
VALUES
(
'Dr. Ananya Sharma',
'ananya.doctor@example.com',
'9000000001',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),
(
'Dr. Rohit Mehta',
'rohit.doctor@example.com',
'9000000002',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),
(
'Dr. Neha Patil',
'neha.doctor@example.com',
'9000000003',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),(
'Dr. Arjun Rao',
'arjun.doctor@example.com',
'9000000004',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),
(
'Dr. Kavya Iyer',
'kavya.doctor@example.com',
'9000000005',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),
(
'Dr. Sameer Khan',
'sameer.doctor@example.com',
'9000000006',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
),
(
'Dr. Priya Nair',
'priya.doctor@example.com',
'9000000007',
'DOCTOR',
'$2y$10$9vkfZH2psSI9/RAWTZ8bCOAt7J40JPHyhJEPg82RhsR4AvFZza71S'
);Insert doctor profiles
4
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'Cardiologist', 12, 1800.00, NULL
FROM users WHERE email = 'ananya.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'Orthopedic', 8, 1200.00, NULL
FROM users WHERE email = 'rohit.doctor@example.com'
AND NOT EXISTS ( SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'Dentist', 6, 800.00, NULL
FROM users WHERE email = 'neha.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'General Physician', 10, 700.00, NULL
FROM users WHERE email = 'arjun.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)SELECT id, 'Cardiologist', 5, 1400.00, NULL
FROM users WHERE email = 'kavya.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'Orthopedic', 15, 2000.00, NULL
FROM users WHERE email = 'sameer.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);
INSERT INTO doctor_profiles
(user_id, specialization, experience_years, consultation_fee, photo_url)
SELECT id, 'Dentist', 9, 1000.00, NULL
FROM users WHERE email = 'priya.doctor@example.com'
AND NOT EXISTS (
SELECT 1
FROM doctor_profiles
WHERE user_id = users.id
);Verify doctor data once
5
GET http://localhost:8080/api/doctors?page=0&size=6
totalElements = 7 totalPages = 2
Start frontend testing
6
http://localhost:5173
http://localhost:8080
Test 1 — Patient registration
Name: Rahul Patil Email: rahul.patient@example.com Phone: 9876543210 Password: Patient@123
Registration successful Role = PATIENT
Test 2 — Patient login
rahul.patient@example.com Patient@123
Test 3 — My Profile
Patient Dashboard → My Profile
Rahul Patil rahul.patient@example.com 9876543210 PATIENT
Test 4 — Doctor listing
Patient Dashboard → Find Doctor
Search by name Specialization Maximum fee Sort by fee Sort by name Sort by experience Ascending Descending Reset Filters
Page 1 Page 2
Test 5 — Book appointment
Dr. Ananya Sharma Cardiologist
Future date 10:00 AMAppointment Created PENDING UNPAID
Test 6 — Razorpay payment
success@razorpay
failure@razorpay
Appointment Status: CONFIRMED Payment Status: PAID
Test 7 — Email
Patient name Doctor Specialization Date Slot Fee CONFIRMED PAID
Test 8 — My Appointments
Patient Dashboard → My Appointments
Dr. Ananya Sharma CONFIRMED PAID
Test 9 — Doctor login
ananya.doctor@example.com Doctor@123
Test 10 — Doctor profile
Doctor Dashboard → My Profile
Cardiologist 12 years ₹1800
Experience: 12 → 13
Test 11 — Doctor schedule
Doctor Dashboard → My Schedule
CONFIRMED + PAID
Mark Completed
COMPLETED + PAID
Test 12 — Prescription
Add Prescription
Diagnosis: Seasonal viral fever Doctor Notes: Drink plenty of fluids and take adequate rest.
Medicine Name: Paracetamol Dosage: 500 mg Frequency: Twice daily Duration: 3 days
Medicine Name: Vitamin C Dosage: 500 mg Frequency: Once daily Duration: 5 days
Test 13 — Patient views prescription
rahul.patient@example.com Patient@123
COMPLETED PAID
View Prescription
Doctor name Diagnosis Paracetamol Vitamin C Dosage Frequency Duration Doctor Notes
Test 14 — Cancellation flow
Dr. Rohit Mehta
PENDING UNPAID
CANCELLED UNPAID
public LocalDateTime getPaidAt() {
return paidAt;
}
public void setPaidAt(LocalDateTime paidAt) {
this.paidAt = paidAt;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}DTO 1 — PaymentOrderResponse.java
4
dto/PaymentOrderResponse.java
package com.medicare.dto;
public class PaymentOrderResponse {
private Long appointmentId;
private String razorpayOrderId;
private long amount;
private String currency;
private String keyId;
public PaymentOrderResponse(Long appointmentId, String razorpayOrderId,
long amount, String currency, String keyId) {
this.appointmentId = appointmentId;
this.razorpayOrderId = razorpayOrderId;
this.amount = amount;
this.currency = currency;
this.keyId = keyId;
}
public Long getAppointmentId() {
return appointmentId;
}
public String getRazorpayOrderId() {
return razorpayOrderId;
} public long getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
public String getKeyId() {
return keyId;
}
}DTO 2 — PaymentVerificationRequest.java
5
dto/PaymentVerificationRequest.java
package com.medicare.dto;import jakarta.validation.constraints.NotBlank;
public class PaymentVerificationRequest {
@NotBlank(message = "Razorpay payment ID is required")
private String razorpayPaymentId;
@NotBlank(message = "Razorpay signature is required")
private String razorpaySignature;
public PaymentVerificationRequest() {
}
public String getRazorpayPaymentId() {
return razorpayPaymentId;
}
public void setRazorpayPaymentId(String razorpayPaymentId) {
this.razorpayPaymentId = razorpayPaymentId;
}
public String getRazorpaySignature() {
return razorpaySignature;
}
public void setRazorpaySignature(String razorpaySignature) {
this.razorpaySignature = razorpaySignature;
}
}Create PaymentException.java
6
package com.medicare.exception;
public class PaymentException
extends RuntimeException {
public PaymentException(
String message) {
super(message);
}
}Add PaymentException handler
7
@ExceptionHandler(PaymentException.class)
public ResponseEntity<ErrorResponse> handlePaymentException(
PaymentException exception, HttpServletRequest request) {
ErrorResponse errorResponse = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_GATEWAY.value(),
exception.getMessage(),
request.getRequestURI()
);
return ResponseEntity
.status(HttpStatus.BAD_GATEWAY)
.body(errorResponse);
}Create PaymentService.java
8
service/PaymentService.java
package com.medicare.service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.medicare.dto.AppointmentResponse;
import com.medicare.dto.PaymentOrderResponse;
import com.medicare.dto.PaymentVerificationRequest;
import com.medicare.entity.Appointment;
import com.medicare.entity.AppointmentStatus;
import com.medicare.entity.PaymentStatus;
import com.medicare.entity.User;
import com.medicare.exception.AppointmentNotFoundException;
import com.medicare.exception.PaymentException;
import com.medicare.exception.UserNotFoundException;import com.medicare.repository.AppointmentRepository;
import com.medicare.repository.UserRepository;
import com.razorpay.Order;
import com.razorpay.Payment;
import com.razorpay.RazorpayClient;
import com.razorpay.RazorpayException;
import com.razorpay.Utils;
@Service
public class PaymentService {
private final AppointmentRepository appointmentRepository;
private final UserRepository userRepository;
private final AppointmentService appointmentService;
@Value("${razorpay.key-id}")
private String keyId;
@Value("${razorpay.key-secret}")
private String keySecret;
public PaymentService(AppointmentRepository appointmentRepository,
UserRepository userRepository, AppointmentService appointmentService) {
this.appointmentRepository = appointmentRepository;
this.userRepository = userRepository;
this.appointmentService = appointmentService;
} // -------------------------------------------------
// CREATE RAZORPAY ORDER
// -------------------------------------------------
@Transactional
@PreAuthorize("hasRole('PATIENT')")
public PaymentOrderResponse createOrder(Long appointmentId, String email) {
User patient = findUserByEmail(email);
Appointment appointment = findAppointmentById(appointmentId);
verifyOwnership(appointment, patient);
validateForPayment(appointment);
long amount = convertToPaise(appointment.getConsultationFee());
/*
* If an order already exists,
* reuse it for payment retry.
*/
if (appointment.getRazorpayOrderId() !=
null && !appointment.getRazorpayOrderId().isBlank()) {
return new PaymentOrderResponse(
appointment.getId(),
appointment.getRazorpayOrderId(),
amount,"INR",keyId );
}
try {
RazorpayClient client = new RazorpayClient(keyId, keySecret);
JSONObject orderRequest = new JSONObject();
orderRequest.put("amount", amount);
orderRequest.put("currency", "INR");
orderRequest.put("receipt", "appt_" + appointment.getId());
JSONObject notes = new JSONObject();
notes.put("appointmentId", appointment.getId());
orderRequest.put("notes", notes);
Order order = client.orders.create(orderRequest);
String razorpayOrderId = order.get("id");
appointment.setRazorpayOrderId(razorpayOrderId);
appointmentRepository.save(appointment);
return new PaymentOrderResponse(
appointment.getId(), razorpayOrderId, amount,"INR",keyId
); } catch (RazorpayException exception) {
throw new PaymentException(
"Unable to create payment order: " + getRazorpayMessage(exception)
);
}
}
// -------------------------------------------------
// VERIFY PAYMENT
// -------------------------------------------------
@Transactional
@PreAuthorize("hasRole('PATIENT')")
public AppointmentResponse verifyPayment(Long appointmentId,
PaymentVerificationRequest request, String email) {
User patient = findUserByEmail(email);
Appointment appointment = findAppointmentById(appointmentId);
verifyOwnership(appointment, patient);
/*
* Makes verification safe
* if it is accidentally retried.
*/
if (appointment.getPaymentStatus() == PaymentStatus.PAID) {
return appointmentService.getAppointmentById(appointmentId, email);
} if (appointment.getAppointmentStatus() != AppointmentStatus.PENDING) {
throw new IllegalArgumentException(
"Only pending appointments can be paid");
}
if (appointment.getPaymentStatus() != PaymentStatus.UNPAID) {
throw new IllegalArgumentException(
"Appointment is not available for payment");
}
if (appointment.getRazorpayOrderId() ==
null || appointment.getRazorpayOrderId().isBlank()) {
throw new IllegalArgumentException(
"Payment order has not been created");
}
try {
// -----------------------------------------
// 1. VERIFY RAZORPAY SIGNATURE
// -----------------------------------------
JSONObject attributes = new JSONObject();
/*
* IMPORTANT:
* Use order ID stored in database.
* Do not trust the browser order ID.
*/ attributes.put("razorpay_order_id", appointment.getRazorpayOrderId());
attributes.put("razorpay_payment_id", request.getRazorpayPaymentId());
attributes.put("razorpay_signature", request.getRazorpaySignature());
boolean validSignature =
Utils.verifyPaymentSignature(attributes, keySecret);
if (!validSignature) {
throw new IllegalArgumentException("Invalid payment signature");
}
// -----------------------------------------
// 2. FETCH PAYMENT DIRECTLY FROM RAZORPAY
// -----------------------------------------
RazorpayClient client = new RazorpayClient(keyId, keySecret);
Payment payment = client.payments.fetch(request.getRazorpayPaymentId());
String paymentOrderId = payment.get("order_id");
Number paymentAmount = payment.get("amount");
String paymentCurrency = payment.get("currency");
String razorpayStatus = payment.get("status");
long expectedAmount = convertToPaise(appointment.getConsultationFee()); // -----------------------------------------
// 3. VERIFY ORDER
// -----------------------------------------
if (!appointment.getRazorpayOrderId().equals(paymentOrderId)) {
throw new IllegalArgumentException(
"Payment order does not match appointment");
}
// -----------------------------------------
// 4. VERIFY AMOUNT
// -----------------------------------------
if (paymentAmount ==
null || paymentAmount.longValue() != expectedAmount) {
throw new IllegalArgumentException(
"Payment amount does not match consultation fee");
}
// -----------------------------------------
// 5. VERIFY CURRENCY
// -----------------------------------------
if (!"INR".equalsIgnoreCase(paymentCurrency)) {
throw new IllegalArgumentException("Invalid payment currency");
} // -----------------------------------------
// 6. VERIFY PAYMENT CAPTURED
// -----------------------------------------
if (!"captured".equalsIgnoreCase(razorpayStatus)) {
throw new IllegalArgumentException("Payment has not been captured");
}
// -----------------------------------------
// 7. UPDATE APPOINTMENT
// -----------------------------------------
appointment.setRazorpayPaymentId(request.getRazorpayPaymentId());
appointment.setPaymentStatus(PaymentStatus.PAID);
appointment.setAppointmentStatus(AppointmentStatus.CONFIRMED);
appointment.setPaidAt(LocalDateTime.now());
appointmentRepository.save(appointment);
/*
* Reuse our existing DTO.
* No PaymentVerificationResponse DTO.
*/
return appointmentService.getAppointmentById(appointmentId, email);
} catch (RazorpayException exception) {
throw new PaymentException( "Unable to verify payment: " + getRazorpayMessage(exception)
);
}
}
// -------------------------------------------------
// HELPERS
// -------------------------------------------------
private User findUserByEmail(String email) {
return userRepository
.findByEmailIgnoreCase(email)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
private Appointment findAppointmentById(Long appointmentId) {
return appointmentRepository
.findById(appointmentId)
.orElseThrow(() -> new AppointmentNotFoundException(
"Appointment not found with id: " + appointmentId));
}
private void verifyOwnership(Appointment appointment, User patient) {
if (!appointment.getPatient().getId().equals(patient.getId())) { throw new AccessDeniedException(
"You cannot make payment for this appointment");
}
}
private void validateForPayment(Appointment appointment) {
if (appointment.getAppointmentStatus() != AppointmentStatus.PENDING) {
throw new IllegalArgumentException(
"Only pending appointments can be paid");
}
if (appointment.getPaymentStatus() != PaymentStatus.UNPAID) {
throw new IllegalArgumentException(
"Appointment is already paid or unavailable for payment");
}
}
private long convertToPaise(BigDecimal amount) {
return amount.movePointRight(2).longValueExact();
}
private String getRazorpayMessage(RazorpayException exception) {
String description = exception.getDescription(); if (description != null && !description.isBlank()) {
return description;
}
return exception.getMessage();
}
}Create PaymentController.java
9
package com.medicare.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.dto.AppointmentResponse;
import com.medicare.dto.PaymentOrderResponse;
import com.medicare.dto.PaymentVerificationRequest;
import com.medicare.service.PaymentService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(
PaymentService paymentService) {
this.paymentService = paymentService;
}
// ---------------------------------------------
// CREATE RAZORPAY ORDER
// --------------------------------------------- @PostMapping("/{appointmentId}/order")
public ResponseEntity<PaymentOrderResponse>
createOrder(
@PathVariable Long appointmentId,
Authentication authentication) {
return ResponseEntity.ok(
paymentService.createOrder(appointmentId,authentication.getName())
);
}
// ---------------------------------------------
// VERIFY RAZORPAY PAYMENT
// ---------------------------------------------
@PostMapping("/{appointmentId}/verify")
public ResponseEntity<AppointmentResponse>
verifyPayment(
@PathVariable Long appointmentId,
@Valid
@RequestBody
PaymentVerificationRequest request,
Authentication authentication) {
return ResponseEntity.ok(
paymentService.verifyPayment(
appointmentId,request,authentication.getName()) );
}
}Update SecurityConfig
10
.anyRequest() .denyAll()
// Patient payment APIs
.requestMatchers( HttpMethod.POST,"/api/payments/**")
.hasRole("PATIENT")// Patient or assigned doctor can view
// prescription for an appointment
.requestMatchers(HttpMethod.GET,"/api/prescriptions/appointment/*")
.hasAnyRole("PATIENT","DOCTOR")
// Patient payment APIs
.requestMatchers(HttpMethod.POST,"/api/payments/**")
.hasRole("PATIENT")
.anyRequest()
.denyAll()Tighten appointment cancellation
11
@PreAuthorize("hasRole('PATIENT')")
public AppointmentResponse
cancelAppointment(Long appointmentId, String email) {
User patient = findUserByEmail(email);
Appointment appointment = findAppointmentById(appointmentId);
if (!appointment.getPatient().getId().equals(patient.getId())) {
throw new AccessDeniedException(
"You cannot cancel this appointment"
);
}
if (appointment.getAppointmentStatus()==AppointmentStatus.COMPLETED) {
throw new IllegalArgumentException(
"Completed appointment cannot be cancelled"
);
} if (appointment.getAppointmentStatus() == AppointmentStatus.CANCELLED) {
throw new IllegalArgumentException(
"Appointment is already cancelled"
);
}
/*
* Refund flow is not part
* of this project.
*/
if (appointment.getPaymentStatus() == PaymentStatus.PAID) {
throw new IllegalArgumentException(
"Paid appointment cannot be cancelled"
);
}
appointment.setAppointmentStatus(AppointmentStatus.CANCELLED);
return mapToAppointmentResponse(appointmentRepository.save(appointment));
}Tighten doctor completion
12
@PreAuthorize("hasRole('DOCTOR')")
public AppointmentResponse completeAppointment(Long appointmentId, String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId());
Appointment appointment = findAppointmentById(appointmentId);
if (!appointment.getDoctor().getId().equals(doctor.getId())) {
throw new AccessDeniedException("You cannot complete this appointment");
}
if (appointment.getAppointmentStatus() == AppointmentStatus.CANCELLED) {
throw new IllegalArgumentException(
"Cancelled appointment cannot be completed");
}
if (appointment.getAppointmentStatus() == AppointmentStatus.COMPLETED) {
throw new IllegalArgumentException("Appointment is already completed");
}
/*
* After Phase 13, consultation can be completed only after successful payment.
*/ if (appointment.getAppointmentStatus() != AppointmentStatus.CONFIRMED ||
appointment.getPaymentStatus() != PaymentStatus.PAID) {
throw new IllegalArgumentException(
"Only confirmed and paid appointments can be completed");
}
appointment.setAppointmentStatus(AppointmentStatus.COMPLETED);
return mapToAppointmentResponse(appointmentRepository.save(appointment));
}FRONTEND
Final index.html
13
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title> Medicare Healthcare </title>
</head>
<body>
<div id="root"></div>
<!-- Razorpay Standard Checkout -->
<script src="https://checkout.razorpay.com/v1/checkout.js"> </script>
<script type="module" src="/src/main.jsx"> </script>
</body>
</html>Create paymentService.js
14
src/services/paymentService.js
import api, { getAuthConfig } from "../api/api";
const createOrder = async (appointmentId) => {
const response = await api.post(`/payments/${appointmentId}/order`,
{},
getAuthConfig()
);
return response.data;
};
const verifyPayment = async (appointmentId, paymentData) => {const verifyPayment = async (appointmentId, paymentData) => {
const response = await api.post(`/payments/${appointmentId}/verify`,
paymentData,
getAuthConfig()
);
return response.data;
};
const paymentService = { createOrder, verifyPayment};
export default paymentService;Create reusable PaymentButton.jsx
15
src/components/payment/PaymentButton.jsx
import { useState } from "react";
import { useAuth } from "../../context/AuthContext";
import paymentService from "../../services/paymentService";
function PaymentButton({ appointment, onPaymentSuccess,
className = "btn btn-success w-100" }) {
const { user } = useAuth();
const [paying, setPaying] = useState(false);
const [error, setError] = useState("");
const handlePayment = async () => {
setError("");
if (!window.Razorpay) {
setError("Razorpay Checkout could not be loaded.");
return;
}
try {
setPaying(true);
// ----------------------------------
// CREATE ORDER ON BACKEND
// ---------------------------------- const order = await paymentService.createOrder(appointment.id);
let verificationStarted = false;
const options = {
key: order.keyId,
amount: order.amount,
currency: order.currency,
name: "Medicare Healthcare",
description: `Consultation with ${appointment.doctorName}`,
order_id: order.razorpayOrderId,
// ----------------------------------
// PAYMENT SUCCESS
// ----------------------------------
handler: async (response) => {
verificationStarted = true;
setError("");
try {
const updatedAppointment =
await paymentService.verifyPayment(
appointment.id,
{
razorpayPaymentId: response.razorpay_payment_id,
razorpaySignature: response.razorpay_signature
}
);
if (onPaymentSuccess) {
onPaymentSuccess(updatedAppointment);
}
} catch (error) {
setError(
error.response?.data?.message ||
"Payment verification failed"
);
} finally {
setPaying(false);
}
},
// ----------------------------------
// PREFILL
// ----------------------------------
prefill: {
name: user?.name || "",
email: user?.email || ""
},
// ----------------------------------
// MODAL SETTINGS
// ----------------------------------
modal: {
confirm_close: true,
ondismiss: () => {
if (!verificationStarted) {
setPaying(false);
}
}
}
};
const razorpay = new window.Razorpay(options);
razorpay.on("payment.failed", (response) => {
setError(
response.error?.description ||
"Payment failed. Please try again."
);
});
razorpay.open();
} catch (error) {
setPaying(false);
setError(
error.response?.data?.message ||
"Unable to start payment"
);
}
};
return (
<div>
<button type="button" className={className} onClick={handlePayment} disabled={paying}>
{
paying
? "Processing Payment..."
: "Pay Now"
}
</button>
{
error && (
<div className="alert alert-danger mt-2 mb-0">
{error}
</div>
)
}
</div>
);
}
export default PaymentButton; if (onPaymentSuccess) {
onPaymentSuccess(updatedAppointment);
}
} catch (error) {
setError(
error.response?.data?.message ||
"Payment verification failed"
);
} finally {
setPaying(false);
}
},
// ----------------------------------
// PREFILL
// ----------------------------------
prefill: {
name: user?.name || "",
email: user?.email || ""
},
// ----------------------------------
// MODAL SETTINGS
// ---------------------------------- modal: {
confirm_close: true,
ondismiss: () => {
if (!verificationStarted) {
setPaying(false);
}
}
}
};
const razorpay = new window.Razorpay(options);
razorpay.on("payment.failed", (response) => {
setError(
response.error?.description ||
"Payment failed. Please try again."
);
});
razorpay.open();
} catch (error) {
setPaying(false); setError(
error.response?.data?.message ||
"Unable to start payment"
);
}
};
return (
<div>
<button type="button" className={className}
onClick={handlePayment} disabled={paying}>
{
paying
? "Processing Payment..."
: "Pay Now"
}
</button>
{
error && (
<div className="alert alert-danger mt-2 mb-0">
{error}
</div>
)
}
</div>
);}
export default PaymentButton;Final AppointmentCard.jsx
16
import PaymentButton from "../payment/PaymentButton";
function AppointmentCard({ appointment, onCancel, cancellingId,
onViewPrescription, onPaymentSuccess }) {
const formatTimeSlot = (timeSlot) => {
switch (timeSlot) { case "MORNING_10_AM":
return "10:00 AM";
case "AFTERNOON_2_PM":
return "2:00 PM";
case "EVENING_5_PM":
return "5:00 PM";
default:
return timeSlot;
}
};
const getAppointmentBadge = (status) => {
switch (status) {
case "PENDING":
return "text-bg-warning";
case "CONFIRMED":
return "text-bg-primary";
case "COMPLETED":
return "text-bg-success";
case "CANCELLED":
return "text-bg-secondary"; default:
return "text-bg-light";
}
};
const getPaymentBadge = (status) => {
switch (status) {
case "PAID":
return "text-bg-success";
case "FAILED":
return "text-bg-danger";
case "UNPAID":
return "text-bg-warning";
default:
return "text-bg-light";
}
};
const canPay =
appointment.appointmentStatus === "PENDING" &&
appointment.paymentStatus === "UNPAID"; /*
* Refunds are not part
* of the project.
*
* So paid appointments
* cannot be cancelled.
*/
const canCancel = canPay;
return (
<div className="card shadow-sm h-100">
<div className="card-body">
<div
className="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 className="mb-1">
{appointment.doctorName}
</h5>
<p className="text-primary mb-0">
{appointment.specialization}
</p>
</div> <span className={`badge ${getAppointmentBadge(
appointment.appointmentStatus)}`}>
{appointment.appointmentStatus}
</span>
</div>
<hr />
<p className="mb-2">
<strong>Appointment ID:</strong>{" "}
{appointment.id}
</p>
<p className="mb-2">
<strong>Date:</strong>{" "}
{appointment.appointmentDate}
</p>
<p className="mb-2">
<strong>Time:</strong>{" "}
{formatTimeSlot(appointment.timeSlot)}
</p>
<p className="mb-2"> <strong>Consultation Fee:</strong>{" "}
₹{appointment.consultationFee}
</p>
<p className="mb-4">
<strong>Payment:</strong>{" "}
<span className={`badge ${getPaymentBadge(
appointment.paymentStatus)}`}>
{appointment.paymentStatus}
</span>
</p>
{/* PAYMENT + CANCEL */}
{
canPay && (
<div className="d-grid gap-2">
<PaymentButton appointment={appointment}
onPaymentSuccess={onPaymentSuccess} />
<button
type="button"
className="btn btn-outline-danger"
onClick={() => onCancel(appointment.id)}
disabled={cancellingId === appointment.id}> {
cancellingId === appointment.id
? "Cancelling..."
: "Cancel Appointment"
}
</button>
</div>
)
}
{/* CONFIRMED */}
{
appointment.appointmentStatus === "CONFIRMED" &&
appointment.paymentStatus === "PAID" && (
<div className="alert alert-success mb-0">
Payment completed.
Appointment confirmed.
</div>
)
}
{/* VIEW PRESCRIPTION */}
{
appointment.appointmentStatus === "COMPLETED" && (
<button type="button" className="btn btn-outline-primary w-100"
onClick={() => onViewPrescription(appointment.id)}
>
View Prescription
</button>
)
}
{/* CANCELLED */}
{
appointment.appointmentStatus === "CANCELLED" && (
<div className="alert alert-secondary mb-0">
This appointment
has been cancelled.
</div>
)
}
</div>
</div>
);
}
export default AppointmentCard;Update MyAppointments.jsx
17
const handlePaymentSuccess =
(updatedAppointment) => {
setAppointments( previousAppointments =>
previousAppointments.map(appointment =>
appointment.id === updatedAppointment.id
? updatedAppointment : appointment
)
);
};<AppointmentCard
appointment={appointment}
onCancel={handleCancel}
cancellingId={cancellingId}
onViewPrescription={handleViewPrescription}
onPaymentSuccess={handlePaymentSuccess}
/>Update BookAppointment.jsx
18
import PaymentButton from "../../components/payment/PaymentButton";const [success, setSuccess] = useState(null);const handlePaymentSuccess = (updatedAppointment) => {
setSuccess(updatedAppointment);
};{success && (
<div className={success.paymentStatus === "PAID" ? "alert alert-success" :
"alert alert-info"}>
<h5>
{success.paymentStatus === "PAID" ? "Appointment Confirmed" :
"Appointment Created"}
</h5>
<p className="mb-1"> Appointment ID: <strong>{success.id}</strong> </p>
<p className="mb-1"> Doctor: <strong>{success.doctorName}</strong> </p>
<p className="mb-1"> Date: <strong>{success.appointmentDate}</strong> </p>
<p className="mb-1"> Status: <strong>{success.appointmentStatus}</strong></p>
<p className="mb-3"> Payment: <strong>{success.paymentStatus}</strong> </p>
{success.paymentStatus ===
"UNPAID" && success.appointmentStatus === "PENDING" && (
<PaymentButton appointment={success}
onPaymentSuccess={handlePaymentSuccess} />
)}
{success.paymentStatus === "PAID" && (
<Link to="/patient/appointments" className="btn btn-primary"> View My Appointments
</Link>
)}
</div>
)}{!success && (
<div className="card shadow-sm">
{/* your existing booking form */}
</div>
)}Book ↓ Appointment Created ↓ Pay Now ↓ Razorpay ↓ Appointment Confirmed
Final DoctorAppointmentCard payment rule
19
const canComplete = appointment.appointmentStatus !== "COMPLETED"
&&
appointment.appointmentStatus !== "CANCELLED";const canComplete = appointment.appointmentStatus ==="CONFIRMED"
&&
appointment.paymentStatus ==="PAID";{
appointment.appointmentStatus ==="PENDING"
&&
appointment.paymentStatus ==="UNPAID"
&& (
<div className="alert alert-warning mb-0"> Waiting for patient payment. </div>
)
}import { Link } from "react-router";
function DoctorAppointmentCard({ appointment, onComplete, completingId }) {
// -------------------------------------------------
// FORMAT TIME SLOT
// -------------------------------------------------
const formatTimeSlot = (timeSlot) => {
switch (timeSlot) {
case "MORNING_10_AM":
return "10:00 AM";
case "AFTERNOON_2_PM":
return "2:00 PM";
case "EVENING_5_PM":
return "5:00 PM";
default:
return timeSlot;
}
}; // -------------------------------------------------
// APPOINTMENT STATUS BADGE
// -------------------------------------------------
const getStatusBadge = (status) => {
switch (status) {
case "PENDING":
return "text-bg-warning";
case "CONFIRMED":
return "text-bg-primary";
case "COMPLETED":
return "text-bg-success";
case "CANCELLED":
return "text-bg-secondary";
default:
return "text-bg-light";
}
};
// -------------------------------------------------
// CAN DOCTOR COMPLETE?
// -------------------------------------------------
const canComplete =
appointment.appointmentStatus === "CONFIRMED" &&
appointment.paymentStatus === "PAID"; // -------------------------------------------------
// WAITING FOR PAYMENT?
// -------------------------------------------------
const waitingForPayment =
appointment.appointmentStatus === "PENDING" &&
appointment.paymentStatus === "UNPAID";
return (
<div className="card shadow-sm h-100">
<div className="card-body">
{/* PATIENT + STATUS */}
<div
className="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 className="mb-1">
{appointment.patientName}
</h5>
<small className="text-secondary">
Patient
</small>
</div>
<span className={`badge ${getStatusBadge(
appointment.appointmentStatus)}`}>
{appointment.appointmentStatus} </span>
</div>
<hr />
{/* APPOINTMENT ID */}
<p className="mb-2">
<strong>Appointment ID:</strong>{" "}
{appointment.id}
</p>
{/* DATE */}
<p className="mb-2">
<strong>Date:</strong>{" "}
{appointment.appointmentDate}
</p>
{/* TIME */}
<p className="mb-2">
<strong>Time:</strong>{" "}
{formatTimeSlot(appointment.timeSlot)}
</p> {/* CONSULTATION FEE */}
<p className="mb-2">
<strong>Consultation Fee:</strong>{" "}
₹{appointment.consultationFee}
</p>
{/* PAYMENT STATUS */}
<p className="mb-4">
<strong>Payment:</strong>{" "}
{appointment.paymentStatus}
</p>
{/* WAITING FOR PATIENT PAYMENT */}
{waitingForPayment && (
<div className="alert alert-warning mb-0">
Waiting for patient payment.
</div>
)}
{/* MARK COMPLETED */}
{canComplete && (
<button
type="button" className="btn btn-success w-100"
onClick={() => onComplete(appointment.id)}
disabled={completingId === appointment.id}
>
{completingId === appointment.id ? "Completing..." :
"Mark Completed"}
</button>
)}
{/* PRESCRIPTION */}
{appointment.appointmentStatus === "COMPLETED" && (
<Link
to={`/doctor/appointments/${appointment.id}/prescription`}
className="btn btn-primary w-100"
>
Add Prescription
</Link>
)}
{/* CANCELLED */}
{appointment.appointmentStatus === "CANCELLED" && (
<div className="alert alert-secondary mb-0">
Patient cancelled this appointment.
</div>
)} </div>
</div>
);
}
export default DoctorAppointmentCard;PENDING + UNPAID → Waiting for patient payment CONFIRMED + PAID → Mark Completed COMPLETED + PAID → Add Prescription
Razorpay Dashboard setting
20
status = captured
How to test Phase 13
21
1. Start the backend and verify the new database columns.
2. Login as PATIENT and book an appointment (PENDING + UNPAID).
3. Click Pay Now and complete a successful Razorpay test payment.
4. Verify that the appointment becomes CONFIRMED + PAID, with payment details saved.
5. Refresh My Appointments and confirm the status remains unchanged and the Cancel button disappears.
6. Login as DOCTOR, mark the appointment as completed, and verify COMPLETED + PAID.
7. Confirm the prescription flow works normally.
8. Test failed or closed payments; the appointment should remain PENDING + UNPAID, allowing retry or cancellation.
9. Verify that another patient cannot create or verify payments for someone else's appointment.Booking ↓ PENDING + UNPAID ↓ Pay Now ↓ Razorpay Checkout ↓ Backend signature verification ↓ Backend payment verification ↓ CONFIRMED + PAID ↓ Doctor completes
↓ COMPLETED + PAID ↓ Prescription
We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully integrated the Razorpay Payment Gateway into the Medicare application!.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) React Basics & Backend Integration (Spring Boot)
By Content ITV