# Payment System Implementation Plan ## File Structure Overview ``` src/ ├── app/ │ ├── api/ │ │ └── payments/ │ │ ├── route.js # GET all, POST create │ │ ├── [id]/ │ │ │ └── route.js # GET, PUT, DELETE single payment │ │ ├── class/ │ │ │ └── [classId]/ │ │ │ └── route.js # GET payments by class │ │ └── user/ │ │ └── [userId]/ │ │ └── route.js # GET payments by user │ │ │ ├── (protected)/ │ │ ├── admin/ │ │ │ └── dashboard/ │ │ │ └── payments/ │ │ │ ├── page.jsx # Main dashboard │ │ │ ├── components/ │ │ │ │ ├── PaymentsTable.jsx │ │ │ │ ├── PaymentDetail.jsx │ │ │ │ ├── PaymentStats.jsx │ │ │ │ └── VerifyPaymentButton.jsx │ │ │ │ │ ├── dashboard/ │ │ │ ├── guardian/ │ │ │ │ ├── payments/ │ │ │ │ │ ├── page.jsx │ │ │ │ │ └── register/ │ │ │ │ │ ├── page.jsx │ │ │ │ │ └── components/ │ │ │ │ │ ├── PaymentForm.jsx │ │ │ │ │ └── ReceiptUpload.jsx │ │ │ │ └── class/ │ │ │ │ └── [id]/ │ │ │ │ └── payments/ │ │ │ │ └── page.jsx │ │ │ │ │ │ │ └── student/ │ │ │ └── payments/ │ │ │ ├── page.jsx │ │ │ └── register/ │ │ │ ├── page.jsx │ │ │ └── components/ │ │ │ ├── PaymentForm.jsx │ │ │ └── ReceiptUpload.jsx │ │ │ │ │ └── components/ │ │ └── shared/ │ │ └── PaymentStatusBadge.jsx # Reusable badge component │ │ │ └── (protected)/dashboard/guardian/class/[id]/ │ └── page.jsx # Update to show payment status │ ├── models/ │ └── Payment.js # New payment model │ └── lib/ └── utils/ └── payments.js # Payment helper functions ``` ## Detailed Implementation Steps ### Step 1: Create Payment Model **File**: `src/app/models/Payment.js` ```javascript import mongoose from "mongoose"; import connectDB from "@/app/config/mongodb"; const PaymentSchema = new mongoose.Schema({ classId: { type: mongoose.Schema.Types.ObjectId, ref: "Class", required: [true, "Class is required"], index: true, }, userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: [true, "User is required"], index: true, }, amount: { type: Number, required: [true, "Amount is required"], min: [0, "Amount must be positive"], }, paymentDate: { type: Date, required: [true, "Payment date is required"], default: Date.now, }, paymentMethod: { type: String, enum: ["pix", "bank_transfer", "cash", "credit_card", "debit_card"], required: [true, "Payment method is required"], }, status: { type: String, enum: ["pending", "verified", "rejected"], default: "pending", index: true, }, receiptUrl: { type: String, }, notes: { type: String, maxlength: 500, }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, }); PaymentSchema.index({ classId: 1, userId: 1 }); PaymentSchema.index({ status: 1 }); PaymentSchema.pre("save", function(next) { this.updatedAt = Date.now(); next(); }); const getPaymentModel = async () => { await connectDB(); if (process.env.NODE_ENV === "development") { delete mongoose.connection.models["Payment"]; } return mongoose.models.Payment || mongoose.model("Payment", PaymentSchema); }; export { PaymentSchema, getPaymentModel }; ``` ### Step 2: Create Payment API Routes #### 2.1 Main Payments Route **File**: `src/app/api/payments/route.js` ```javascript import connectDB from "@/app/config/mongodb"; import { getPaymentModel } from "@/app/models/Payment"; import { getClassModel } from "@/app/models/Class"; import { getUserModel } from "@/app/models/User"; import { NextResponse } from "next/server"; import { auth } from "@/app/lib/utils/auth"; // GET all payments (admin only) export async function GET(request) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only admins can view all payments if (!currentUser.roles.includes("admin")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const payments = await Payment.find({}) .populate("classId", "classTitle") .populate("userId", "fullName") .sort({ createdAt: -1 }) .lean(); return NextResponse.json(payments, { status: 200 }); } catch (error) { console.error("Error fetching payments:", error); return NextResponse.json( { success: false, message: "Error fetching payments", error: error.message }, { status: 500 } ); } } // POST create payment (guardian/student only) export async function POST(request) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only guardians and students can create payments if (!currentUser.roles.includes("guardian") && !currentUser.roles.includes("student")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const Class = await getClassModel(); const body = await request.json(); const { classId, amount, paymentDate, paymentMethod, receiptUrl, notes } = body; // Validate class belongs to user's wards (if guardian) or is enrolled (if student) const classData = await Class.findById(classId); if (!classData) { return NextResponse.json( { success: false, message: "Class not found" }, { status: 404 } ); } // Check if user is guardian and class belongs to their ward if (currentUser.roles.includes("guardian")) { const isWard = currentUser.wardAccounts.some(wardId => wardId.toString() === classId); if (!isWard) { return NextResponse.json( { success: false, message: "You can only register payments for your wards' classes" }, { status: 403 } ); } } // Check if user is student and enrolled in class if (currentUser.roles.includes("student")) { const isEnrolled = classData.students.some(studentId => studentId.toString() === session.user.id ); if (!isEnrolled) { return NextResponse.json( { success: false, message: "You are not enrolled in this class" }, { status: 403 } ); } } const payment = await Payment.create({ classId, userId: session.user.id, amount, paymentDate: new Date(paymentDate), paymentMethod, receiptUrl, notes, }); const populatedPayment = await Payment.findById(payment._id) .populate("classId", "classTitle") .populate("userId", "fullName") .lean(); return NextResponse.json( { success: true, message: "Payment registered successfully", data: populatedPayment }, { status: 201 } ); } catch (error) { console.error("Error creating payment:", error); return NextResponse.json( { success: false, message: "Error creating payment", error: error.message }, { status: 500 } ); } } ``` #### 2.2 Single Payment Route **File**: `src/app/api/payments/[id]/route.js` ```javascript import connectDB from "@/app/config/mongodb"; import { getPaymentModel } from "@/app/models/Payment"; import { getUserModel } from "@/app/models/User"; import { NextResponse } from "next/server"; import { auth } from "@/app/lib/utils/auth"; // GET single payment (admin only) export async function GET(request, { params }) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only admins can view payment details if (!currentUser.roles.includes("admin")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const payment = await Payment.findById(params.id) .populate("classId", "classTitle") .populate("userId", "fullName") .lean(); if (!payment) { return NextResponse.json( { success: false, message: "Payment not found" }, { status: 404 } ); } return NextResponse.json(payment, { status: 200 }); } catch (error) { console.error("Error fetching payment:", error); return NextResponse.json( { success: false, message: "Error fetching payment", error: error.message }, { status: 500 } ); } } // PUT update payment status (admin only) export async function PUT(request, { params }) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only admins can update payment status if (!currentUser.roles.includes("admin")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const body = await request.json(); const { status, notes } = body; const payment = await Payment.findByIdAndUpdate( params.id, { status, notes, updatedAt: Date.now() }, { new: true } ).populate("classId", "classTitle").populate("userId", "fullName").lean(); if (!payment) { return NextResponse.json( { success: false, message: "Payment not found" }, { status: 404 } ); } return NextResponse.json( { success: true, message: "Payment updated successfully", data: payment }, { status: 200 } ); } catch (error) { console.error("Error updating payment:", error); return NextResponse.json( { success: false, message: "Error updating payment", error: error.message }, { status: 500 } ); } } // DELETE payment (admin only) export async function DELETE(request, { params }) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only admins can delete payments if (!currentUser.roles.includes("admin")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const payment = await Payment.findByIdAndDelete(params.id); if (!payment) { return NextResponse.json( { success: false, message: "Payment not found" }, { status: 404 } ); } return NextResponse.json( { success: true, message: "Payment deleted successfully" }, { status: 200 } ); } catch (error) { console.error("Error deleting payment:", error); return NextResponse.json( { success: false, message: "Error deleting payment", error: error.message }, { status: 500 } ); } } ``` #### 2.3 Payments by Class Route **File**: `src/app/api/payments/class/[classId]/route.js` ```javascript import connectDB from "@/app/config/mongodb"; import { getPaymentModel } from "@/app/models/Payment"; import { getUserModel } from "@/app/models/User"; import { NextResponse } from "next/server"; import { auth } from "@/app/lib/utils/auth"; // GET payments for a specific class (admin only) export async function GET(request, { params }) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only admins can view class payments if (!currentUser.roles.includes("admin")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); const payments = await Payment.find({ classId: params.classId }) .populate("userId", "fullName") .sort({ createdAt: -1 }) .lean(); return NextResponse.json(payments, { status: 200 }); } catch (error) { console.error("Error fetching class payments:", error); return NextResponse.json( { success: false, message: "Error fetching payments", error: error.message }, { status: 500 } ); } } ``` #### 2.4 Payments by User Route **File**: `src/app/api/payments/user/[userId]/route.js` ```javascript import connectDB from "@/app/config/mongodb"; import { getPaymentModel } from "@/app/models/Payment"; import { getUserModel } from "@/app/models/User"; import { NextResponse } from "next/server"; import { auth } from "@/app/lib/utils/auth"; // GET payments for a specific user (guardian/student only) export async function GET(request, { params }) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 }); } const user = await getUserModel(); const currentUser = await user.findOne({ _id: session.user.id }); // Only guardians and students can view their own payments if (!currentUser.roles.includes("guardian") && !currentUser.roles.includes("student")) { return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 403 }); } await connectDB(); try { const Payment = await getPaymentModel(); // If guardian, show payments for all their wards if (currentUser.roles.includes("guardian")) { const payments = await Payment.find({ userId: { $in: currentUser.wardAccounts } }) .populate("classId", "classTitle") .sort({ createdAt: -1 }) .lean(); return NextResponse.json(payments, { status: 200 }); } // If student, show only their own payments if (currentUser.roles.includes("student")) { const payments = await Payment.find({ userId: session.user.id }) .populate("classId", "classTitle") .sort({ createdAt: -1 }) .lean(); return NextResponse.json(payments, { status: 200 }); } } catch (error) { console.error("Error fetching user payments:", error); return NextResponse.json( { success: false, message: "Error fetching payments", error: error.message }, { status: 500 } ); } } ``` ### Step 3: Create Helper Functions **File**: `src/lib/utils/payments.js` ```javascript import { getPaymentModel } from "@/app/models/Payment"; import { getClassModel } from "@/app/models/Class"; // Get payment status for a user in a class export async function getPaymentStatus(userId, classId) { const Payment = await getPaymentModel(); const payment = await Payment.findOne({ userId, classId }).sort({ createdAt: -1 }).lean(); if (!payment) { return { status: "not_paid", amount: null }; } return { status: payment.status, amount: payment.amount, paymentDate: payment.paymentDate, paymentMethod: payment.paymentMethod, }; } // Get all payments for a class with user details export async function getClassPayments(classId) { const Payment = await getPaymentModel(); const payments = await Payment.find({ classId }) .populate("userId", "fullName") .sort({ createdAt: -1 }) .lean(); return payments; } // Get payment statistics export async function getPaymentStats() { const Payment = await getPaymentModel(); const totalPayments = await Payment.countDocuments(); const verifiedPayments = await Payment.countDocuments({ status: "verified" }); const pendingPayments = await Payment.countDocuments({ status: "pending" }); const rejectedPayments = await Payment.countDocuments({ status: "rejected" }); return { totalPayments, verifiedPayments, pendingPayments, rejectedPayments, verificationRate: totalPayments > 0 ? Math.round((verifiedPayments / totalPayments) * 100) : 0, }; } ``` ### Step 4: Create Admin Dashboard Components #### 4.1 Payments Table **File**: `src/app/(protected)/admin/dashboard/payments/components/PaymentsTable.jsx` ```javascript import { useState } from "react"; import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; export default function PaymentsTable({ payments }) { const [filter, setFilter] = useState("all"); const filteredPayments = payments.filter(payment => { if (filter === "all") return true; return payment.status === filter; }); const getStatusBadge = (status) => { const styles = { pending: "bg-yellow-100 text-yellow-800", verified: "bg-green-100 text-green-800", rejected: "bg-red-100 text-red-800", }; return ( {status.charAt(0).toUpperCase() + status.slice(1)} ); }; return (
{filteredPayments.map((payment) => ( ))}
Aluno Classe Valor Data Método Status
{payment.userId?.fullName || "N/A"} {payment.classId?.classTitle || "N/A"} R$ {payment.amount.toFixed(2)} {format(new Date(payment.paymentDate), "dd/MM/yyyy", { locale: ptBR })} {payment.paymentMethod} {getStatusBadge(payment.status)}
{filteredPayments.length === 0 && (
Nenhum pagamento encontrado
)}
); } ``` #### 4.2 Payment Stats **File**: `src/app/(protected)/admin/dashboard/payments/components/PaymentStats.jsx` ```javascript export default function PaymentStats({ stats }) { const statsCards = [ { title: "Total de Pagamentos", value: stats.totalPayments, color: "blue", icon: "💳", }, { title: "Verificados", value: stats.verifiedPayments, color: "green", icon: "✅", }, { title: "Pendentes", value: stats.pendingPayments, color: "yellow", icon: "⏳", }, { title: "Rejeitados", value: stats.rejectedPayments, color: "red", icon: "❌", }, ]; return (
{statsCards.map((card) => (

{card.title}

{card.value}

{card.icon}
))}

Taxa de Verificação

{stats.verificationRate}%

📊
); } ``` ### Step 5: Create Guardian Payment Interface #### 5.1 Payment Form **File**: `src/app/(protected)/dashboard/guardian/payments/register/components/PaymentForm.jsx` ```javascript import { useState } from "react"; import { useRouter } from "next/navigation"; import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; export default function PaymentForm({ classId, className }) { const router = useRouter(); const [formData, setFormData] = useState({ amount: "", paymentDate: new Date().toISOString().split("T")[0], paymentMethod: "pix", receipt: null, notes: "", }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(""); try { const formDataToSend = new FormData(); formDataToSend.append("classId", classId); formDataToSend.append("amount", parseFloat(formData.amount)); formDataToSend.append("paymentDate", formData.paymentDate); formDataToSend.append("paymentMethod", formData.paymentMethod); if (formData.receipt) { formDataToSend.append("receipt", formData.receipt); } if (formData.notes) { formDataToSend.append("notes", formData.notes); } const response = await fetch("/api/payments", { method: "POST", body: formDataToSend, }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || "Erro ao registrar pagamento"); } router.push(`/dashboard/guardian/payments?success=true`); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return (

Registrar Pagamento - {className}

{error && (
{error}
)}
setFormData({ ...formData, amount: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0,00" />
setFormData({ ...formData, paymentDate: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
setFormData({ ...formData, receipt: e.target.files[0] })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />

Formatos aceitos: JPG, PNG, PDF (máx. 5MB)