From 8d691eebbb87c442d5e5952fbb8d54d1d1791699 Mon Sep 17 00:00:00 2001 From: anishshobithps Date: Thu, 9 May 2024 07:43:10 +0530 Subject: [PATCH] feat: send email via paymentID --- src/app/admin/razorpay/[id]/page.tsx | 187 ++++++++++-- src/app/admin/razorpay/page.tsx | 289 +++++++++++++----- .../api/admin/sendEmail/[paymentId]/route.ts | 115 +++++++ src/app/api/razorpay/verify/route.ts | 20 +- src/lib/utils.ts | 18 ++ 5 files changed, 502 insertions(+), 127 deletions(-) create mode 100644 src/app/api/admin/sendEmail/[paymentId]/route.ts diff --git a/src/app/admin/razorpay/[id]/page.tsx b/src/app/admin/razorpay/[id]/page.tsx index 18cbd68..711de09 100644 --- a/src/app/admin/razorpay/[id]/page.tsx +++ b/src/app/admin/razorpay/[id]/page.tsx @@ -1,34 +1,167 @@ "use client"; + +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; import React, { useEffect, useState } from "react"; +import { toast } from "@/components/ui/use-toast"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; + +export interface Root { + id: string; + entity: string; + amount: number; + currency: string; + status: string; + order_id: string; + invoice_id: any; + international: boolean; + method: string; + amount_refunded: number; + refund_status: any; + captured: boolean; + description: string; + card_id: any; + bank: any; + wallet: any; + vpa: string; + email: string; + contact: string; + notes: Notes; + fee: number; + tax: number; + error_code: any; + error_description: any; + error_source: any; + error_step: any; + error_reason: any; + acquirer_data: AcquirerData; + created_at: number; + upi: Upi; +} + +export interface Notes { + customerName: string; + customerEmail: string; + customerContact: string; + college: string; + events: string; +} + +export interface AcquirerData { + rrn: string; + upi_transaction_id: string; +} + +export interface Upi { + vpa: string; +} + +async function sendEmail(paymentId: string) { + const res = await fetch(`/api/admin/sendEmail/${paymentId}`, { + method: "POST", + }); + if (res.status === 200) { + toast({ + title: "Email sent", + description: "Email sent successfully", + }); + } else { + toast({ + title: "Error", + description: "Error sending email", + variant: "destructive", + }); + } +} function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { - const { id } = params; - const [paymentData, setPaymentData] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const res = await fetch(`/api/razorpay/${id}`); - const data = await res.json(); - setPaymentData(data); - } catch (error) { - console.error('Error fetching payment data:', error); - } - }; - - fetchData(); - - // Cleanup function if necessary - return () => { - // Any cleanup code can go here - }; - }, [id]); - return ( -
-

Fetch Razorpay Payment Data

-
{paymentData ? JSON.stringify(paymentData, null, 2) : 'Loading...'}
-
- ); + const { id } = params; + const [paymentData, setPaymentData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch(`/api/razorpay/${id}`); + const data = await res.json(); + setPaymentData(data); + } catch (error) { + console.error("Error fetching payment data:", error); + } + }; + fetchData(); + }, [id]); + return ( + + + Payment Data of {paymentData?.notes.customerName} + + + + + + + + + + + + + + + ); } export default FetchRazorpayPaymentData; diff --git a/src/app/admin/razorpay/page.tsx b/src/app/admin/razorpay/page.tsx index 74a25df..6f97658 100644 --- a/src/app/admin/razorpay/page.tsx +++ b/src/app/admin/razorpay/page.tsx @@ -1,6 +1,5 @@ "use client"; import { Input } from "@/components/ui/input"; -import React, { useEffect, useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -15,21 +14,79 @@ import { FormMessage, } from "@/components/ui/form"; import { useRouter } from "next/navigation"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { AdvancedDataTable } from "@/components/datatable"; +import { ColumnDef } from "@tanstack/react-table"; +import { useMemo, useState, useEffect } from "react"; +import { DataTableCheckBox } from "@/components/datatable/data-table-checkbox"; + +export interface Root { + entity: string; + count: number; + items: Item[]; +} + +export interface Item { + id: string; + entity: string; + amount: number; + currency: string; + status: string; + order_id: string; + invoice_id: any; + international: boolean; + method: string; + amount_refunded: number; + refund_status: any; + captured: boolean; + description: string; + card_id: any; + bank: any; + wallet?: string; + vpa?: string; + email: string; + contact: string; + notes: any; + fee: number; + tax: number; + error_code: any; + error_description: any; + error_source: any; + error_step: any; + error_reason: any; + acquirer_data: AcquirerData; + created_at: number; + upi?: Upi; +} + +export interface AcquirerData { + rrn?: string; + upi_transaction_id?: string; + transaction_id: any; +} + +export interface Upi { + vpa: string; +} const formSchema = z.object({ razorpayPaymentId: z.string(), }); +async function getData() { + const response = await fetch("/api/razorpay"); + const json = await response.json(); + return json; +} + function FetchRazorpayPaymentData() { + const filename = "razorpay"; + const [data, setData] = useState([]); const router = useRouter(); + + useEffect(() => { + getData().then(setData); + }, []); + const form = useForm>({ resolver: zodResolver(formSchema), }); @@ -38,81 +95,85 @@ function FetchRazorpayPaymentData() { router.push(`/admin/razorpay/${values.razorpayPaymentId}`); } - const [paymentData, setPaymentData] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const res = await fetch(`/api/razorpay`); - const data = await res.json(); - setPaymentData(data); - } catch (error) { - console.error("Error fetching payment data:", error); - } - }; + const columns = useMemo[]>( + () => [ + { + id: "select", + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + size: 20, + }, + { + header: "Name", + accessorKey: "items.notes.customerName", + id: "name", + cell: (info) => info.getValue(), + }, + { + accessorKey: "email", + id: "email", + header: "Email", + meta: { + filterVariant: "text", + }, + }, + { + accessorKey: "events", + id: "events", + header: "Events", + meta: { + filterVariant: "text", + }, + }, + { + accessorKey: "college", + id: "college", + header: "College", + meta: { + filterVariant: "text", + }, + }, + { + accessorKey: "role", + id: "role", + header: "Role", + meta: { + filterVariant: "select", + }, + }, + { + accessorKey: "contact", + id: "contact", + header: "Contact", + meta: { + filterVariant: "text", + }, + }, + ], + [] + ); - fetchData(); + const flattenedData = data.flatMap((rootItem) => rootItem.items); - // Cleanup function if necessary - return () => { - // Any cleanup code can go here - }; - }, []); return ( -
-

Fetch Razorpay Payment Data

- - - - Name - RazorpaymentID - Email - Time - Phone - College - Events - Amount - - - - {paymentData ? ( - paymentData.items.map((item: any) => ( - - {item.notes.customerName} - {item.id} - {item.email} - - {new Date(item.created_at * 1000).toLocaleString(undefined, { - dateStyle: "full", - timeStyle: "long", - })} - - {item.notes.customerContact} - {item.notes.college} - - {item.notes.events - .split(",") - .map((event: string, i: number) => ( -
  • - {event} -
  • - ))} -
    - - {new Intl.NumberFormat("en-IN", { - style: "currency", - currency: "INR", - }).format(item.amount / 100)} - -
    - )) - ) : ( - - Loading... - - )} -
    -
    + <>

    Search by Razorpay Payment ID

    @@ -135,7 +196,71 @@ function FetchRazorpayPaymentData() {
    -
    +{/* + + columns={columns} + data={flattenedData} + exportProps={{ + exportFileName: filename, + }} + /> */} + + + //
    + //

    Fetch Razorpay Payment Data

    + // + // + // + // Name + // RazorpaymentID + // Email + // Time + // Phone + // College + // Events + // Amount + // + // + // + // {paymentData ? ( + // paymentData.items.map((item: any) => ( + // + // {item.notes.customerName} + // {item.id} + // {item.email} + // + // {new Date(item.created_at * 1000).toLocaleString(undefined, { + // dateStyle: "full", + // timeStyle: "long", + // })} + // + // {item.notes.customerContact} + // {item.notes.college} + // {/* + // {(item.notes.events || []) + // .split(",") + // .map((event: string, i: number) => ( + //
  • + // {event} + //
  • + // ))} + //
    */} + // + // {new Intl.NumberFormat("en-IN", { + // style: "currency", + // currency: "INR", + // }).format(item.amount / 100)} + // + //
    + // )) + // ) : ( + // + // Loading... + // + // )} + //
    + //
    + //
    ); } diff --git a/src/app/api/admin/sendEmail/[paymentId]/route.ts b/src/app/api/admin/sendEmail/[paymentId]/route.ts new file mode 100644 index 0000000..661644c --- /dev/null +++ b/src/app/api/admin/sendEmail/[paymentId]/route.ts @@ -0,0 +1,115 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { PaymentStatus, UserRole } from "@prisma/client"; +import { auth } from "@/auth"; +import { sendEmail } from "@/helper/mailer"; +import { razorpay } from "@/lib/razorpay"; +import { generatedSignature } from "@/lib/utils"; + +export async function POST( + request: NextRequest, + context: { params: { paymentId: string } } +) { + const session = await auth(); + + if (!session) { + return NextResponse.json( + { message: "Unauthorized", isOk: false }, + { status: 401 } + ); + } + + if (session.user.role !== UserRole.ADMIN) { + return NextResponse.json( + { message: "Forbidden", isOk: false }, + { status: 403 } + ); + } + + const { paymentId } = context.params; + const payment = await razorpay.payments.fetch(paymentId); + const signature = generatedSignature(payment.order_id, payment.id); + + const user = await prisma.user.findUnique({ + where: { + email: payment.notes.customerEmail, + }, + }); + + const prismaPayment = await prisma.payment.findUnique({ + where: { + razorpayPaymentId: payment.id, + }, + }); + + try { + await sendEmail({ + amount: parseFloat(payment.amount.toString()), + email: session.user?.email!, + teamNames: [], + contactNumber: payment.contact.toString(), + name: payment.notes.customerName, + events: payment.notes.events.split(","), + registrationLink: `https://tiarasjec.in/api/verify/${user?.id}`, + }); + } catch (error) { + console.error(error); + } + + if (!prismaPayment) { + await prisma.$transaction(async (prisma) => { + await prisma.payment.create({ + data: { + amount: parseFloat(payment.amount.toString()), + signature, + razorpayPaymentId: payment.id, + orderCreationId: payment.order_id, + status: PaymentStatus.SUCCESS, + user: { + connect: { + email: session.user?.email!, + }, + }, + }, + }); + + const existingUser = await prisma.user.findUnique({ + where: { + email: session.user?.email!, + }, + include: { + teams: true, + }, + }); + + const mergedEvents = [ + ...existingUser?.events!, + ...payment.notes.events.split(","), + ]; + await prisma.user.update({ + where: { + email: session.user?.email!, + }, + data: { + registrationEmailSent: true, + contact: payment.contact.toString(), + college: payment.notes.college, + events: mergedEvents, + teams: { + createMany: { + data: [], + }, + }, + }, + include: { + teams: true, + }, + }); + }); + } + + return NextResponse.json( + { message: "Payment verified successfully", isOk: true }, + { status: 200 } + ); +} diff --git a/src/app/api/razorpay/verify/route.ts b/src/app/api/razorpay/verify/route.ts index a5fe30d..9633937 100644 --- a/src/app/api/razorpay/verify/route.ts +++ b/src/app/api/razorpay/verify/route.ts @@ -1,27 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import crypto from "crypto"; import { prisma } from "@/lib/prisma"; import { PaymentStatus } from "@prisma/client"; import { auth } from "@/auth"; import { sendEmail } from "@/helper/mailer"; import { razorpay } from "@/lib/razorpay"; - -const generatedSignature = ( - razorpayOrderId: string, - razorpayPaymentId: string -) => { - const keySecret = process.env.RAZORPAY_SECRET; - if (!keySecret) { - throw new Error( - "Razorpay key secret is not defined in environment variables." - ); - } - const sig = crypto - .createHmac("sha256", keySecret) - .update(razorpayOrderId + "|" + razorpayPaymentId) - .digest("hex"); - return sig; -}; +import { generatedSignature } from "@/lib/utils"; interface PaymentResponse { amount: number; @@ -49,6 +32,7 @@ export async function POST(request: NextRequest) { ); } + const signature = generatedSignature( data.orderCreationId, data.razorpayPaymentId diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0897a08..b30b897 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,7 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { toast } from "@/components/ui/use-toast"; +import crypto from "crypto"; import { Teams } from "./interfaces"; import { FilterFn, SortingFn, sortingFns } from "@tanstack/table-core"; import { compareItems, rankItem } from "@tanstack/match-sorter-utils"; @@ -37,6 +38,23 @@ export const fuzzySort: SortingFn = (rowA, rowB, columnId) => { return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir; }; +export const generatedSignature = ( + razorpayOrderId: string, + razorpayPaymentId: string +) => { + const keySecret = process.env.RAZORPAY_SECRET; + if (!keySecret) { + throw new Error( + "Razorpay key secret is not defined in environment variables." + ); + } + const sig = crypto + .createHmac("sha256", keySecret) + .update(razorpayOrderId + "|" + razorpayPaymentId) + .digest("hex"); + return sig; +}; + export const baseURL = process.env.NEXT_PUBLIC_URL ? `https://${process.env.NEXT_PUBLIC_URL}/` : process.env.NEXT_PUBLIC_VERCEL_URL