Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/bank-webhook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,9 @@ app.post("/webhook/qr-payment", async (req, res) => {
}
});

app.listen(3003, () => console.log("Webhook backend running on port 3003"));
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});

const PORT = process.env.PORT || 3002;
app.listen(PORT, () => console.log(`Webhook backend running on port ${PORT}`));
36 changes: 36 additions & 0 deletions apps/merchant-app/@/components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../../lib/utils"


const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}

export { Badge, badgeVariants }
73 changes: 35 additions & 38 deletions apps/merchant-app/app/api/bills/route.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@

import { NextResponse } from "next/server";
import { PrismaClient } from "@repo/db/client";

const prisma = new PrismaClient();

export async function POST(request: Request) {
try{
const { searchParams } = new URL(request.url);
const merchantId = searchParams.get("merchantId");
if(!merchantId){
return NextResponse.json({ message: "Merchant ID is required" }, { status: 400 });
}
const merchant = await prisma.merchant.findUnique({
where: { id: parseInt(merchantId) }
});

if(!merchant){
return NextResponse.json({ message: "Merchant not found" }, { status: 404 });
}

const bills = await prisma.billSchedule.findMany({
where: { merchantId: parseInt(merchantId) },
orderBy: { dueDate: "asc" },
});
return NextResponse.json(bills, { status: 200 });
} catch (error) {
console.error("Error fetching bills:", error);
return NextResponse.json({ error: "Failed to fetch bills" }, { status: 500 });
const prisma = new PrismaClient();

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const merchantId = searchParams.get("merchantId");
if (!merchantId) {
return NextResponse.json({ message: "Merchant ID required" }, { status: 400 });
}

const bills = await prisma.billSchedule.findMany({
where: { merchantId: parseInt(merchantId) },
orderBy: { dueDate: "asc" },
});

return NextResponse.json(bills, { status: 200 });
} catch (error) {
console.error("Error:", error);
return NextResponse.json({ error: "Failed" }, { status: 500 });
}
}

export async function PATCH(request: Request) {
try{
const { billId, status } = await request.json();
if(!billId || !["PAID", "OVERDUE", "CANCELLED"].includes(status)){
return NextResponse.json({ message: "Invalid input" }, { status: 400 });
}
const bill = await prisma.billSchedule.update({
where: { id: parseInt(billId) },
data: { status }
});
return NextResponse.json({ message: "Bill status updated", bill }, { status: 200 });
} catch (error) {
console.error("Error updating bill status:", error);
return NextResponse.json({ error: "Failed to update bill status" }, { status: 500 });
try {
const { billId, status } = await request.json();
if (!billId || !["PAID", "OVERDUE", "CANCELLED"].includes(status)) {
return NextResponse.json({ message: "Invalid" }, { status: 400 });
}

const bill = await prisma.billSchedule.update({
where: { id: parseInt(billId) },
data: { status },
});

return NextResponse.json({ message: "Updated", bill }, { status: 200 });
} catch (error) {
console.error("Error:", error);
return NextResponse.json({ error: "Failed" }, { status: 500 });
}
}
4 changes: 2 additions & 2 deletions apps/merchant-app/app/api/qr/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export async function POST(request: Request) {
return NextResponse.json({ error: `Merchant not found for email: ${session.user.email}` }, { status: 404 });
}

const upiId = merchant.upiId && merchant.upiId.length <= 20 ? merchant.upiId : merchant.email.slice(0, 20);
const upiId = "7822952595@ibl";
const merchantName = merchant.name && merchant.name.length <= 15 ? merchant.name : "Merchant";
const qrId = generateShortId(); // Short 10-char ID
const qrId = generateShortId();

const upiUri = `upi://pay?pa=${encodeURIComponent(upiId)}&pn=${encodeURIComponent(
merchantName
Expand Down
166 changes: 80 additions & 86 deletions apps/merchant-app/app/bills/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@repo/ui/button";
import { motion } from "framer-motion";
import { format } from "date-fns";

type BillSchedule = {
id: number;
Expand All @@ -19,103 +21,95 @@ type BillSchedule = {

export default function MerchantBillsPage() {
const [bills, setBills] = useState<BillSchedule[]>([]);
const merchantId = 1; // Replace with authenticated merchant ID
const merchantId = 1;

useEffect(() => {
const fetchBills = async () => {
try {
const response = await fetch(`/api/bills?merchantId=${merchantId}`);
const data = await response.json();
setBills(data);
} catch (error) {
console.error("Error fetching merchant bills:", error);
}
};
fetchBills();
fetch(`/api/bills?merchantId=${merchantId}`)
.then((r) => r.json())
.then(setBills)
.catch(() => setBills([]));
}, []);

const handleStatusUpdate = async (billId: number, status: "PAID" | "OVERDUE") => {
try {
const response = await fetch("/api/bills", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ billId, status }),
});
if (response.ok) {
setBills(bills.map(bill => bill.id === billId ? { ...bill, status } : bill));
}
} catch (error) {
console.error("Error updating bill status:", error);
}
const updateStatus = async (billId: number, status: "PAID" | "OVERDUE") => {
await fetch("/api/bills", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ billId, status }),
});
setBills(bills.map((b) => (b.id === billId ? { ...b, status } : b)));
};

return (
<div className=" selection:bg-zinc-500 selection:text-white min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 p-8">
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 text-white p-8">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-extrabold text-gray-800 mb-8">Merchant Bill Management</h1>
<div className="space-y-6">
<h2 className="text-2xl font-semibold text-gray-700">All Bills ({bills.length})</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{bills.map((bill) => {
const dueDate = new Date(bill.dueDate);
const isOverdue = dueDate < new Date() && bill.status !== "PAID";
{/* HEADER */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<h1 className="text-5xl font-black bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">
Merchant Bill Dashboard
</h1>
<p className="text-xl text-cyan-200 mt-4">Total Bills: {bills.length}</p>
</motion.div>

return (
<div
key={bill.id}
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
>
<h3 className="text-lg font-semibold text-gray-800">{bill.billType}</h3>
<p className="text-sm text-gray-500">Provider: {bill.provider}</p>
<p className="text-sm text-gray-500">User ID: {bill.userId}</p>
<p className="text-xl font-bold text-blue-600 mt-2">
₹{(bill.amount / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-600 mt-1">
Due: {dueDate.toLocaleDateString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
<p className="text-sm text-gray-600">
Status: <span className={`font-medium ${
bill.status === "PAID" ? "text-green-600" :
isOverdue ? "text-red-600" : "text-yellow-600"
}`}>{bill.status}</span>
</p>
<p className="text-sm text-gray-600">Payment Method: {bill.paymentMethod}</p>
{bill.nextPayment && (
<p className="text-sm text-gray-600">
Next Payment: {new Date(bill.nextPayment).toLocaleDateString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
)}
{bill.status === "PENDING" && (
<div className="mt-4 flex space-x-2">
<Button
onClick={() => handleStatusUpdate(bill.id, "PAID")}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
>
Mark as Paid
</Button>
<Button
onClick={() => handleStatusUpdate(bill.id, "OVERDUE")}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
>
Mark as Overdue
</Button>
</div>
)}
{/* BILLS GRID */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{bills.map((bill) => {
const due = new Date(bill.dueDate);
const overdue = due < new Date() && bill.status !== "PAID";

return (
<motion.div
key={bill.id}
whileHover={{ scale: 1.03 }}
className="cursor-pointer bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6 shadow-2xl hover:border-cyan-400 transition-all"
>
<div className="flex justify-between items-start mb-3">
<h3 className="text-xl font-bold text-cyan-300">{bill.billType}</h3>
<span
className={`px-3 py-1 rounded-full text-xs font-bold ${
bill.status === "PAID"
? "bg-green-500/20 text-green-300"
: overdue
? "bg-red-500/20 text-red-300"
: "bg-yellow-500/20 text-yellow-300"
}`}
>
{bill.status}
</span>
</div>
);
})}
</div>

<p className="text-sm text-gray-300">Provider: {bill.provider}</p>
<p className="text-3xl font-black text-white mt-3">
₹{(bill.amount / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-400 mt-2">
Due: {format(due, "dd MMM yyyy")}
</p>

{bill.status === "PENDING" && (
<div className="mt-4 flex gap-2">
<Button
onClick={() => updateStatus(bill.id, "PAID")}
className="flex-1 bg-green-600 hover:bg-green-500 text-white font-bold"
>
Paid
</Button>
<Button
onClick={() => updateStatus(bill.id, "OVERDUE")}
className="flex-1 bg-red-600 hover:bg-red-500 text-white font-bold"
>
Overdue
</Button>
</div>
)}
</motion.div>
);
})}
</div>
</div>
</div>
);
}
}
Loading