Enhance payment processing and user interaction features

- Updated createPayment function to log payment data more clearly.
- Introduced verifyPayment function for validating payments via an external API.
- Enhanced DevicesToPay component to include user information and payment verification functionality.
- Added formatDate utility for consistent date formatting across the application.
- Updated Prisma schema to include account number for users.
- Refactored layout and device cart components for improved user experience and responsiveness.
This commit is contained in:
i701 2024-12-09 22:59:13 +05:00
parent 40b40ad3d1
commit 36f22c0614
9 changed files with 218 additions and 51 deletions

View File

@ -5,7 +5,7 @@ import type { PaymentType } from "@/lib/types";
import { revalidatePath } from "next/cache";
export async function createPayment(data: PaymentType) {
console.log("hi", data);
console.log("data", data);
const payment = await prisma.payment.create({
data: {
amount: data.amount,
@ -24,3 +24,48 @@ export async function createPayment(data: PaymentType) {
revalidatePath("/devices");
return payment;
}
type VerifyPaymentType = {
paymentId?: string;
benefName: string;
accountNo?: string;
absAmount: string;
time: string;
};
export async function verifyPayment(data: VerifyPaymentType) {
console.log({ data });
try {
const payment = await prisma.payment.findUnique({
where: {
id: data.paymentId,
},
});
const response = await fetch(
"https://verifypaymentsapi.baraveli.dev/verify-payment",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
},
);
const json = await response.json();
console.log(json);
if (json.success === true) {
await prisma.payment.update({
where: {
id: payment?.id,
},
data: {
paid: true,
},
});
}
revalidatePath("/payment[paymentId]");
return json;
} catch (error) {
console.error(error);
}
}

View File

@ -1,11 +1,22 @@
import DevicesToPay from "@/components/devices-to-pay";
import { auth } from "@/lib/auth";
import { hasSession } from "@/lib/auth-guard";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import React from "react";
export default async function PaymentPage({
params,
}: { params: Promise<{ paymentId: string }> }) {
}: {
params: Promise<{ paymentId: string }>;
}) {
const session = await auth.api.getSession({
headers: await headers()
})
const user = await prisma.user.findUnique({
where: {
id: session?.session.userId
}
})
const paymentId = (await params).paymentId;
const payment = await prisma.payment.findUnique({
where: {
@ -28,6 +39,7 @@ export default async function PaymentPage({
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<DevicesToPay
user={user || undefined}
billFormula={formula ?? undefined}
payment={payment || undefined}
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -8,37 +8,37 @@ import { Toaster } from "sonner";
import "./globals.css";
import QueryProvider from "@/components/query-provider";
const barlow = Barlow({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
variable: "--font-barlow",
subsets: ["latin"],
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
variable: "--font-barlow",
});
export const metadata: Metadata = {
title: "SAR Link Portal",
description: "Sarlink Portal",
title: "SAR Link Portal",
description: "Sarlink Portal",
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${barlow.variable} antialiased font-sans`}>
<Provider>
<NextTopLoader showSpinner={false} zIndex={9999} />
<Toaster richColors />
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
</Provider>
</body>
</html>
);
return (
<html lang="en" suppressHydrationWarning>
<body className={`${barlow.variable} antialiased font-sans`}>
<Provider>
<NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
<Toaster richColors />
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
</Provider>
</body>
</html>
);
}

View File

@ -59,7 +59,7 @@ export function DeviceCartDrawer({
} else {
setMessage("");
}
setTotal(baseAmount + (devices.length - 1) * discountPercentage);
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
}, [months, devices.length, baseAmount, discountPercentage]);
if (pathname === "/payment") {

View File

@ -1,3 +1,5 @@
'use client'
import { verifyPayment } from "@/actions/payment";
import {
Table,
TableBody,
@ -5,35 +7,50 @@ import {
TableCell,
TableFooter,
TableRow,
} from "@/components/ui/table"
import type { BillFormula, Prisma } from "@prisma/client"
import React from 'react'
} from "@/components/ui/table";
import { formatDate } from "@/lib/utils";
import type { BillFormula, Prisma, User } from "@prisma/client";
import { Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "./ui/button";
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true
}
}>
devices: true;
};
}>;
export default function DevicesToPay({ billFormula, payment }: { billFormula?: BillFormula, payment?: PaymentWithDevices }) {
const devices = payment?.devices
export default function DevicesToPay({
billFormula,
payment,
user
}: { billFormula?: BillFormula; payment?: PaymentWithDevices, user?: User }) {
const [verifying, setVerifying] = useState(false)
const devices = payment?.devices;
if (devices?.length === 0) {
return null
return null;
}
const baseAmount = billFormula?.baseAmount ?? 100
const discountPercentage = billFormula?.discountPercentage ?? 75
const baseAmount = billFormula?.baseAmount ?? 100;
const discountPercentage = billFormula?.discountPercentage ?? 75;
// 100+(n1)×75
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage;
return (
<div className='w-full'>
<div className='p-2 flex flex-col gap-2'>
<h3 className='title-bg my-1 font-semibold text-lg'>
{!payment?.paid ? 'Devices to pay' : 'Devices Paid'}
<div className="w-full">
<div className="p-2 flex flex-col gap-2">
<h3 className="title-bg my-1 font-semibold text-lg">
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
</h3>
<div className="flex flex-col gap-2">
{devices?.map((device) => (
<div key={device.id} className="bg-muted border rounded p-2 flex gap-2 items-center">
<div
key={device.id}
className="bg-muted border rounded p-2 flex gap-2 items-center"
>
<div className="flex flex-col">
<div className="text-sm font-medium">{device.name}</div>
<div className="text-xs text-muted-foreground">
@ -44,9 +61,49 @@ export default function DevicesToPay({ billFormula, payment }: { billFormula?: B
))}
</div>
</div>
<div className='m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded'>
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
<Table>
<TableCaption>Please send the following amount to the payment address</TableCaption>
<TableCaption>
<div className="max-w-sm mx-auto">
<p>Please send the following amount to the payment address</p>
<AccountInfomation
accName="Baraveli Dev"
accountNo="90101400028321000"
/>
{payment?.paid ? (
<Button size={"lg"} variant={"secondary"} disabled className="text-green-400 bg-green-800">Payment Verified</Button>
) : (
<Button
disabled={verifying}
onClick={async () => {
setVerifying(true);
const res = await verifyPayment({
paymentId: payment?.id,
benefName: user?.name ?? "",
accountNo: user?.accNo ?? "",
absAmount: String(total),
time: formatDate(new Date(payment?.createdAt || "")),
});
setVerifying(false);
switch (true) {
case res.success === true:
toast.success(res.message);
break;
case res.success === false:
toast.error(res.message);
break;
default:
toast.error("Unexpected error occurred.");
}
}}
size={"lg"} className="mb-4">
{verifying ? "Verifying..." : "Verify Payment"}
{verifying ? <Loader2 className="animate-spin" /> : <Wallet />}
</Button>
)}
</div>
</TableCaption>
<TableBody>
<TableRow>
<TableCell>Total Devices</TableCell>
@ -62,6 +119,44 @@ export default function DevicesToPay({ billFormula, payment }: { billFormula?: B
</Table>
</div>
</div>
)
);
}
function AccountInfomation({
accountNo,
accName,
}: {
accountNo: string;
accName: string;
}) {
const [accNo, setAccNo] = useState(false)
return (
<div className="justify-start items-start border my-4 flex flex-col gap-2 p-2 rounded-md">
<h6 className="title-bg uppercase p-2 border rounded w-full font-semibold">
Account Information
</h6>
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded">
<div className="text-sm font-semibold">Account Name</div>
<span>{accName}</span>
</div>
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded">
<div className="flex flex-col items-start justify-start">
<p className="text-sm font-semibold">Account No</p>
<span>{accountNo}</span>
</div>
<Button
onClick={() => {
setTimeout(() => {
setAccNo(true)
navigator.clipboard.writeText(accountNo)
}, 2000)
toast.success("Account number copied!")
setAccNo((prev) => !prev)
}}
variant={"link"}>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
</Button>
</div>
</div>
);
}

View File

@ -4,3 +4,15 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const formatDate = (date: Date): string => {
const pad = (num: number): string => num.toString().padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1); // Months are zero-based
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes() + 5);
return `${year}-${month}-${day} ${hours}:${minutes}`;
};

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "accNo" TEXT;

View File

@ -20,6 +20,7 @@ model User {
emailVerified Boolean @default(false)
firstPaymentDone Boolean @default(false)
verified Boolean @default(false)
accNo String?
// island String?
address String?
id_card String? @unique