Payment Flow
The complete Razorpay payment flow — from customer click to database update — with real code for each step.
This page walks through the complete payment flow used in EduTrack. Every step is connected to the next. By the end, you will understand exactly what happens between "Parent clicks Pay" and "Fee record status is Paid."
Overview: Five Steps
Each step is a distinct action. Each transition is a network request. Understand this flow before reading the code.
The Paise Reminder
Before the code: every amount passed to Razorpay must be in paise.
Step 1: Parent Clicks "Pay Now" (React Frontend)
Your checkout component handles the button click. It calls an API to create the order, then opens the Razorpay modal.
The Razorpay Checkout SDK (window.Razorpay) is loaded via a <Script> tag in your layout or via a hook. See the openRazorpayModal helper below.
Step 2: Create Order (Edge Function)
Order creation runs on the server. The Razorpay Order is a record that links a specific amount to a specific purpose. The customer's payment is made against this order, which is how Razorpay knows how much to charge.
Always use the amount from your database, not from the request body. If you trust amountInRupees from the client, a technically capable user can modify the request and pay ₹1 for a ₹1,499 fee. The fee structure's amount column is the source of truth. The client's amount is used only for display.
Step 3: Customer Pays (Razorpay Checkout Modal)
The Razorpay Checkout SDK opens a modal over your page. The customer selects a payment method and completes payment. You do not build this UI — Razorpay provides it, and it handles every payment method, every bank, every UPI app.
First, load the Razorpay SDK. In a Next.js project, add this to your root layout:
Then create the helper that opens the modal:
Step 4: Verify Payment (Edge Function)
After the customer pays, Razorpay calls your handler function with three values:
razorpay_payment_id— the unique ID of this paymentrazorpay_order_id— the order ID you created in Step 2razorpay_signature— a cryptographic signature Razorpay generates
You must verify the signature. Without verification, anyone could send fake payment IDs to your server and get their fee marked as paid without paying.
The verification works like this: Razorpay creates the signature by hashing orderId|paymentId with your Key Secret using HMAC-SHA256. You recreate that hash server-side using your Key Secret. If they match, the payment is genuine. If they do not match, the payload was tampered with.
Never mark a fee as "paid" before verifying the signature. A signature mismatch means either a bug in your code or someone attempting to mark a fee as paid without paying. Both deserve investigation. Log the attempt with enough context (fee_record_id, user_id, order_id) to trace it in your logs.
Step 5: Update Fee Record — What Happens in the Database
After signature verification, the flow through the database is:
At this point, the parent's React app receives { success: true } from the edge function, calls onSuccess(), and the UI transitions to the fee payment confirmation screen.
The Payment Flow as a Reconciliation Exercise
For CAs in the group: this flow creates an automatic audit trail.
At any point, you can reconcile:
| Source | What it shows |
|---|---|
payment_transactions table | Every payment attempt, including failures |
fee_records table | Which fees are paid, with payment ID |
| Razorpay Dashboard → Orders | Order created, amount, receipt ID |
| Razorpay Dashboard → Transactions | Payment captured, method, time |
| Razorpay Dashboard → Settlements | When the money arrived in your bank |
A full reconciliation: fee_records table (paid) → payment_transactions (completed) → Razorpay Dashboard (captured) → Bank statement (settled T+3). Every rupee can be traced from parent click to bank deposit.
Common Bugs in This Flow
| Bug | Symptom | Fix |
|---|---|---|
| Amount in rupees instead of paise | Parent is charged ₹2.99 for a ₹299 fee | Use rupeesToPaise() helper — always |
| Trusting client amount | Parent modifies request body to pay ₹1 | Read amount from database, never from request |
| Skipping signature verification | Fake payments accepted | Signature check is mandatory before any database update |
Not recording provider_order_id | Cannot match webhook events to fee records | Always insert to payment_transactions in Step 2 |
Not handling ondismiss | Payment loading spinner stays after user closes modal | Set modal.ondismiss callback |
| Using test keys in production | All real payments fail | Confirm key prefix matches environment |