Skip to content

Razorpay order verification + automatic confirmation emails#79

Merged
kris70lesgo merged 3 commits intokris70lesgo:mainfrom
AnshAggarwal011:feat/payment-email-flow
Oct 22, 2025
Merged

Razorpay order verification + automatic confirmation emails#79
kris70lesgo merged 3 commits intokris70lesgo:mainfrom
AnshAggarwal011:feat/payment-email-flow

Conversation

@AnshAggarwal011
Copy link
Contributor

@AnshAggarwal011 AnshAggarwal011 commented Oct 21, 2025

User description

This PR adds:

  • Razorpay order creation and signature verification (secure payment flow)
  • Automatic confirmation email after successful payment or COD
  • API routes:
    • /api/razorpay/create-order
    • /api/razorpay/verify
    • /api/notify/confirmation
  • Gmail SMTP integration via Nodemailer
  • Updated payment.tsx frontend for new flow

PR Type

Enhancement, Tests


Description

  • Add Razorpay payment gateway integration with order creation and signature verification

  • Implement automatic order confirmation emails via Nodemailer SMTP

  • Create three new API routes for payment flow: create-order, verify, and notify/confirmation

  • Refactor payment.tsx component with complete payment handling logic for both COD and online payments

  • Add nodemailer dependency and update dashboard with improved UI/UX


Diagram Walkthrough

flowchart LR
  A["Payment Options"] --> B{Payment Method}
  B -->|COD| C["notify/confirmation API"]
  B -->|Online| D["create-order API"]
  D --> E["Razorpay Checkout"]
  E --> F["verify API"]
  F --> G["sendOrderEmail"]
  C --> G
  G --> H["Customer Email"]
Loading

File Walkthrough

Relevant files
Enhancement
route.ts
Razorpay order creation endpoint                                                 

src/app/api/razorpay/create-order/route.ts

  • New API endpoint to create Razorpay orders with amount in paise
  • Accepts amount and optional receipt parameters
  • Returns order object with unique order ID for frontend checkout
  • Includes error handling and logging
+21/-0   
route.ts
Razorpay payment signature verification endpoint                 

src/app/api/razorpay/create-order/verify/route.ts

  • Verifies Razorpay payment signatures using HMAC-SHA256
  • Validates razorpay_order_id, razorpay_payment_id, and
    razorpay_signature
  • Sends order confirmation email after successful verification
  • Handles email failures gracefully with logging for retry queue
+56/-0   
route.ts
Order confirmation email notification endpoint                     

src/app/api/notify/confirmation/route.ts

  • New API endpoint for sending order confirmation emails
  • Accepts customer details, order info, and payment method
  • Supports both COD and online payment confirmation flows
  • Includes error handling with graceful fallback messaging
+32/-0   
mailer.ts
Email service with Nodemailer SMTP integration                     

src/lib/mailer.ts

  • Nodemailer transporter configuration with SMTP settings
  • sendOrderEmail function generates HTML email with order details
  • Displays order ID, payment ID, items table, and total amount
  • Supports both COD and online payment email templates
+57/-0   
payment.tsx
Payment component with full Razorpay integration                 

src/components/payment.tsx

  • Complete refactor of payment flow with COD and online payment options
  • Implements Razorpay checkout integration with order creation and
    verification
  • Adds customer data extraction from query parameters (name, email,
    amount, items)
  • Sends confirmation emails via notify/confirmation API for both payment
    methods
  • Adds loading state management and error handling with user-friendly
    messages
  • Improves UI formatting and accessibility with better spacing and
    responsive design
+399/-94
page.tsx
Dashboard with Shop Now button and routing                             

src/app/dashboard/page.tsx

  • Adds useRouter hook for navigation to payment/form pages
  • Implements new "Shop Now" button with minimal design and hover effects
  • Routes to /form with query parameters (noOfPage, totalAmount) for each
    product
  • Adds loading skeleton component with AnimatePresence animation
  • Refactors code formatting and imports for consistency
  • Adds router navigation to AI generator and form pages
+280/-197
Configuration changes
razorpay.ts
Razorpay SDK initialization                                                           

src/lib/razorpay.ts

  • Initializes Razorpay instance with API credentials from environment
  • Exports configured razorpay object for use in API routes
  • Loads RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET from process.env
+6/-0     
Dependencies
package.json
Add nodemailer email library dependency                                   

package.json

  • Adds nodemailer ^7.0.9 dependency for email sending functionality
+1/-0     

@vercel
Copy link

vercel bot commented Oct 21, 2025

@AnshAggarwal011 is attempting to deploy a commit to the agastya's projects Team on Vercel.

A member of the Team first needs to authorize it.

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Oct 21, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Price tampering

Description: The API accepts a client-supplied 'amount' without server-side bounds or validation logic,
allowing potential price tampering (underselling or free orders) if the frontend is
bypassed.
route.ts [6-16]

Referred Code
const { amount, receipt } = await req.json();
// amount should be in INR paise (₹1 = 100)
if (!amount) return NextResponse.json({ error: "amount required" }, { status: 400 });

const order = await razorpay.orders.create({
  amount: Number(amount),
  currency: "INR",
  receipt: receipt ?? `rcpt_${Date.now()}`,
});

return NextResponse.json({ order });
Misconfig validation

Description: SMTP transporter is created directly from environment variables without runtime
validation; missing or incorrect values could cause failures and may leak errors via
logs—validate and fail fast.
mailer.ts [5-10]

Referred Code
export const transporter = nodemailer.createTransport({
  host: SMTP_HOST,
  port: Number(SMTP_PORT ?? 465),
  secure: Number(SMTP_PORT ?? 465) === 465, // true for 465
  auth: { user: SMTP_USER, pass: SMTP_PASS },
});
Secret validation missing

Description: Uses process.env.RAZORPAY_KEY_SECRET without checking existence; if unset, HMAC will use
'undefined' leading to false positives/negatives—must validate presence and fail securely.

route.ts [24-33]

Referred Code
const keySecret = process.env.RAZORPAY_KEY_SECRET as string;
const expected = crypto
  .createHmac("sha256", keySecret)
  .update(`${razorpay_order_id}|${razorpay_payment_id}`)
  .digest("hex");

if (expected !== razorpay_signature) {
  return NextResponse.json({ error: "invalid_signature" }, { status: 400 });
}
HTML injection in email

Description: The email payload fields (customerName, items) are inserted into HTML email without
sanitization, risking HTML injection in outbound emails; sanitize or escape
user-controlled content.
route.ts [12-21]

Referred Code
  await sendOrderEmail({
    to: customerEmail,
    customerName: customerName ?? "Customer",
    orderId,
    paymentId: paymentId ?? (isCOD ? `COD-${orderId}` : "N/A"),
    items: Array.isArray(items) ? items : [],
    totalAmount: Number(totalAmount ?? 0),
    isCOD: Boolean(isCOD),
  });
} catch (mailErr) {
Trusting client inputs

Description: Relies on query parameters (name, email, itemName, qty, pages, amount) to construct order
and email data without validation; a user can manipulate client-side values leading to
mismatched pricing or spoofed recipient—server should source-of-truth validate.
payment.tsx [21-43]

Referred Code
// ---- Read data from query params (with fallbacks) ----
const amountStr = searchParams.get("amount") || "100";
const amountRupees = Number(amountStr) || 100;
const customerName = searchParams.get("name") || "Customer";
const customerEmail = searchParams.get("email") || "customer@example.com";
// Your internal order id (e.g., assignment id). If you don't have one, we generate a temporary one.
const appOrderId =
  searchParams.get("orderId") || `ASHELP-${new Date().getTime()}`;

// Basic item model. You can expand this to match your actual cart.
const items = useMemo(() => {
  const qpName = searchParams.get("itemName");
  const qpQty = Number(searchParams.get("qty") || "1");
  const pages = Number(searchParams.get("pages") || "0");
  // If pages are provided, show that as quantity; else default 1.
  if (qpName) {
    return [{ name: qpName, qty: qpQty > 0 ? qpQty : 1 }];
  }
  if (pages > 0) {
    return [{ name: "Assignment Pages", qty: pages }];
  }


 ... (clipped 2 lines)
Ticket Compliance
🎫 No ticket provided
- [ ] Create ticket/issue <!-- /create_ticket --create_ticket=true -->

</details></td></tr>
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
No custom compliance provided

Follow the guide to enable custom compliance check.

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Oct 21, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Lack of server-side amount validation

The payment verification endpoint trusts the totalAmount from the client. This
is a security risk and should be fixed by fetching the payment details from
Razorpay's API on the server to verify the actual amount paid.

Examples:

src/app/api/razorpay/create-order/verify/route.ts [16-42]
      totalAmount,     // in rupees (for email only)
      orderIdPublic,   // your own app order id (if you have one)
    } = body;

    if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
      return NextResponse.json({ error: "missing_razorpay_params" }, { status: 400 });
    }

    const keySecret = process.env.RAZORPAY_KEY_SECRET as string;
    const expected = crypto

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

// src/app/api/razorpay/create-order/verify/route.ts
export async function POST(req: Request) {
  const {
    razorpay_order_id,
    razorpay_payment_id,
    razorpay_signature,
    totalAmount, // <-- Trusted from client request
  } = await req.json();

  // ... signature verification ...
  const signatureIsValid = (expected === razorpay_signature);

  if (signatureIsValid) {
    // The email confirmation uses the unverified amount from the client.
    await sendOrderEmail({
      // ...
      totalAmount: Number(totalAmount ?? 0),
      // ...
    });
  }
  return NextResponse.json({ ok: true });
}

After:

// src/app/api/razorpay/create-order/verify/route.ts
import { razorpay } from "@/lib/razorpay";

export async function POST(req: Request) {
  const {
    razorpay_order_id,
    razorpay_payment_id,
    razorpay_signature,
  } = await req.json();

  // ... signature verification ...
  const signatureIsValid = (expected === razorpay_signature);

  if (signatureIsValid) {
    // Fetch order and payment from Razorpay to get authoritative data
    const order = await razorpay.orders.fetch(razorpay_order_id);
    const payment = await razorpay.payments.fetch(razorpay_payment_id);

    // Server-side validation of amount and currency
    if (payment.amount !== order.amount || payment.currency !== order.currency) {
      return NextResponse.json({ error: "Tampered amount" }, { status: 400 });
    }

    await sendOrderEmail({
      // ...
      totalAmount: payment.amount / 100, // Use verified amount
      // ...
    });
  }
  return NextResponse.json({ ok: true });
}
Suggestion importance[1-10]: 10

__

Why: This suggestion correctly identifies a critical security vulnerability in the payment verification flow, where the totalAmount is trusted from the client, potentially leading to financial loss.

High
Possible issue
Prevent payment amount tampering vulnerability

Fix a critical vulnerability where the payment amount is read from client-side
URL parameters, allowing price tampering. The server should determine the amount
based on a secure identifier instead of trusting the client.

src/components/payment.tsx [104-134]

     const handleRazorpayPayment = async () => {
       if (busy) return;
       if (!razorpayLoaded || !window.Razorpay) {
         alert("Payment system is still loading. Please try again in a moment.");
         return;
       }
       if (!razorpayKey) {
         alert("Razorpay key is not configured.");
         return;
       }
 
       setBusy(true);
       try {
-        const amountPaise = Math.round(amountRupees * 100);
+        // The amount is no longer sent from the client.
+        // Instead, we send a non-tamperable identifier.
+        // The server will look up the price using this ID.
 
         // 1) Create order on server
         const orderRes = await fetch("/api/razorpay/create-order", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({
-            amount: amountPaise,
+            // Send a product/order ID instead of the amount
+            appOrderId: appOrderId, 
             receipt: `ashelp_${Date.now()}`,
           }),
         });
         const { order, error } = await orderRes.json();
         if (!orderRes.ok || !order?.id) {
           console.error("[create-order] error:", error || orderRes.statusText);
           alert("Could not start payment. Please try again.");
           setBusy(false);
           return;
         }
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 10

__

Why: This suggestion identifies a critical security and business logic flaw where the payment amount is controlled by the client, which could lead to financial loss.

High
Security
Use constant-time comparison for signatures

Replace the standard string comparison for the Razorpay signature with a
constant-time comparison using crypto.timingSafeEqual to prevent timing-based
attacks.

src/app/api/razorpay/create-order/verify/route.ts [24-32]

-    const keySecret = process.env.RAZORPAY_KEY_SECRET as string;
-    const expected = crypto
+    const keySecret = process.env.RAZORPAY_KEY_SECRET;
+    if (!keySecret) {
+      console.error("RAZORPAY_KEY_SECRET is not set");
+      return NextResponse.json({ error: "internal_server_error" }, { status: 500 });
+    }
+
+    const expectedSignature = crypto
       .createHmac("sha256", keySecret)
       .update(`${razorpay_order_id}|${razorpay_payment_id}`)
       .digest("hex");
 
-    if (expected !== razorpay_signature) {
+    const signatureBuffer = Buffer.from(razorpay_signature);
+    const expectedSignatureBuffer = Buffer.from(expectedSignature);
+
+    if (signatureBuffer.length !== expectedSignatureBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedSignatureBuffer)) {
       return NextResponse.json({ error: "invalid_signature" }, { status: 400 });
     }
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion addresses a critical security vulnerability (timing attack) in the payment signature verification process, which is a core part of the PR's functionality.

High
Sanitize variables to prevent HTML injection

Sanitize dynamic data such as customerName and items[].name before embedding it
into the email's HTML content to prevent potential HTML injection
vulnerabilities.

src/lib/mailer.ts [23-49]

+    const escapeHtml = (unsafe: string) => {
+      return unsafe
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;")
+        .replace(/"/g, "&quot;")
+        .replace(/'/g, "&#039;");
+    };
+
     const itemsHtml = items
-      .map(i => `<tr><td style="padding:6px 8px;border:1px solid #e5e7eb">${i.name}</td>
+      .map(i => `<tr><td style="padding:6px 8px;border:1px solid #e5e7eb">${escapeHtml(i.name)}</td>
                      <td style="padding:6px 8px;border:1px solid #e5e7eb;text-align:right">${i.qty}</td></tr>`)
       .join("");
 
     const html = `
       <div style="font-family:Inter,Segoe UI,Arial,sans-serif;max-width:640px;margin:auto">
-        <h2>Thanks for your ${isCOD ? "order (Cash on Delivery)" : "payment"}, ${customerName}!</h2>
+        <h2>Thanks for your ${isCOD ? "order (Cash on Delivery)" : "payment"}, ${escapeHtml(customerName)}!</h2>
         <p>Your order is confirmed. Here are the details:</p>
         <table style="border-collapse:collapse;margin-top:12px">
-          <tr><td style="padding:6px 8px"><b>Order ID</b></td><td style="padding:6px 8px">${orderId}</td></tr>
-          <tr><td style="padding:6px 8px"><b>${isCOD ? "Payment Method" : "Payment ID"}</b></td><td style="padding:6px 8px">${isCOD ? "Cash on Delivery" : paymentId}</td></tr>
+          <tr><td style="padding:6px 8px"><b>Order ID</b></td><td style="padding:6px 8px">${escapeHtml(orderId)}</td></tr>
+          <tr><td style="padding:6px 8px"><b>${isCOD ? "Payment Method" : "Payment ID"}</b></td><td style="padding:6px 8px">${isCOD ? "Cash on Delivery" : escapeHtml(paymentId)}</td></tr>
           <tr><td style="padding:6px 8px"><b>Total</b></td><td style="padding:6px 8px">₹${totalAmount}</td></tr>
         </table>
         <h3 style="margin-top:16px">Items</h3>
         <table style="border-collapse:collapse;border:1px solid #e5e7eb">
           <thead>
             <tr>
               <th style="padding:6px 8px;border:1px solid #e5e7eb;text-align:left">Item</th>
               <th style="padding:6px 8px;border:1px solid #e5e7eb;text-align:right">Qty</th>
             </tr>
           </thead>
           <tbody>${itemsHtml}</tbody>
         </table>
         <p style="margin-top:16px;color:#6b7280">If you have questions, just reply to this email.</p>
       </div>
     `;
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential HTML injection vulnerability in the email generation logic and provides a valid solution, which is an important security best practice.

Medium
  • Update

@AnshAggarwal011
Copy link
Contributor Author

@kris70lesgo please look inti it i have made changes for email notifictaion. Check in the below image:
Screenshot 2025-10-21 215529

@kris70lesgo
Copy link
Owner

@AnshAggarwal011 resolve the conflicts and add the env in .env.example

@AnshAggarwal011
Copy link
Contributor Author

@kris70lesgo Sorry for the inconvenience. I’m currently out of station for 3 days and won’t be able to resolve the issue right now. Please merge it so that it can be reflected for Hacktoberfest.

@kris70lesgo
Copy link
Owner

@AnshAggarwal011 nice work , i see u fixed the most part

@kris70lesgo kris70lesgo merged commit f45f120 into kris70lesgo:main Oct 22, 2025
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants