Files
course-plat/plans/payment-system-implementation.md
2026-08-31 14:10:20 -03:00

1022 lines
31 KiB
Markdown

# 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 (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${styles[status]}`}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
);
};
return (
<div className="overflow-x-auto">
<div className="flex gap-2 mb-4">
<button
onClick={() => setFilter("all")}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
filter === "all" ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"
}`}
>
Todos
</button>
<button
onClick={() => setFilter("pending")}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
filter === "pending" ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"
}`}
>
Pendentes
</button>
<button
onClick={() => setFilter("verified")}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
filter === "verified" ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"
}`}
>
Verificados
</button>
<button
onClick={() => setFilter("rejected")}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
filter === "rejected" ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"
}`}
>
Rejeitados
</button>
</div>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aluno
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Classe
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Valor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Método
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredPayments.map((payment) => (
<tr key={payment._id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{payment.userId?.fullName || "N/A"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{payment.classId?.classTitle || "N/A"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
R$ {payment.amount.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{format(new Date(payment.paymentDate), "dd/MM/yyyy", { locale: ptBR })}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{payment.paymentMethod}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(payment.status)}
</td>
</tr>
))}
</tbody>
</table>
{filteredPayments.length === 0 && (
<div className="text-center py-8 text-gray-500">
Nenhum pagamento encontrado
</div>
)}
</div>
);
}
```
#### 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 (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{statsCards.map((card) => (
<div
key={card.title}
className="bg-white rounded-lg shadow p-6 border-l-4 border-blue-500"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">{card.title}</p>
<p className="text-2xl font-bold text-gray-900 mt-1">
{card.value}
</p>
</div>
<span className="text-3xl">{card.icon}</span>
</div>
</div>
))}
<div className="bg-white rounded-lg shadow p-6 border-l-4 border-purple-500">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Taxa de Verificação</p>
<p className="text-2xl font-bold text-gray-900 mt-1">
{stats.verificationRate}%
</p>
</div>
<span className="text-3xl">📊</span>
</div>
</div>
</div>
);
}
```
### 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 (
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Registrar Pagamento - {className}
</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Valor do Pagamento (R$)
</label>
<input
type="number"
step="0.01"
min="0"
required
value={formData.amount}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Data do Pagamento
</label>
<input
type="date"
required
value={formData.paymentDate}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Método de Pagamento
</label>
<select
required
value={formData.paymentMethod}
onChange={(e) => setFormData({ ...formData, paymentMethod: 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"
>
<option value="pix">PIX</option>
<option value="bank_transfer">Transferência Bancária</option>
<option value="cash">Dinheiro</option>
<option value="credit_card">Cartão de Crédito</option>
<option value="debit_card">Cartão de Débito</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Comprovante (Opcional)
</label>
<input
type="file"
accept="image/*"
onChange={(e) => 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"
/>
<p className="text-sm text-gray-500 mt-1">
Formatos aceitos: JPG, PNG, PDF (máx. 5MB)
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Observações (Opcional)
</label>
<textarea
rows={3}
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: 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="Informações adicionais sobre o pagamento..."
/>
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={loading}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{loading ? "Registrando..." : "Registrar Pagamento"}
</button>
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
</div>
</form>
</div>
</div>
);
}
```
### Step 6: Create Payment Status Badge Component
**File**: `src/app/(protected)/components/shared/PaymentStatusBadge.jsx`
```javascript
export default function PaymentStatusBadge({ status }) {
const styles = {
pending: "bg-yellow-100 text-yellow-800 border-yellow-300",
verified: "bg-green-100 text-green-800 border-green-300",
rejected: "bg-red-100 text-red-800 border-red-300",
not_paid: "bg-gray-100 text-gray-800 border-gray-300",
};
const labels = {
pending: "Pendente",
verified: "Pago",
rejected: "Rejeitado",
not_paid: "Não Pago",
};
return (
<span
className={`px-2 py-1 rounded-full text-xs font-medium border ${styles[status]}`}
>
{labels[status]}
</span>
);
}
```
## Integration Points
### Update Guardian Class Detail Page
Add payment status indicator to the class detail page:
```javascript
// In src/app/(protected)/dashboard/guardian/class/[id]/page.jsx
import PaymentStatusBadge from "@/app/(protected)/components/shared/PaymentStatusBadge";
// After fetching class data, add:
const Payment = await getPaymentModel();
const paymentStatus = await getPaymentStatus(session.user.id, id);
// Add to cls object:
cls.paymentStatus = paymentStatus.status;
cls.paymentAmount = paymentStatus.amount;
// In the JSX:
<PaymentStatusBadge status={cls.paymentStatus} />
```
## Testing Checklist
- [ ] Create payment model and verify database schema
- [ ] Test payment creation API with guardian/student accounts
- [ ] Test payment verification/rejection by admin
- [ ] Test file upload for receipts
- [ ] Verify role-based access control
- [ ] Test payment status indicators in UI
- [ ] Verify payment history display
- [ ] Test payment filtering in admin dashboard
- [ ] Verify payment statistics calculation
- [ ] Test edge cases (invalid amounts, dates, etc.)
## Notes
- The system uses the existing file upload infrastructure
- Payment status workflow: pending → verified/rejected
- Guardians can only register payments for their wards' classes
- Students can only register payments for their enrolled classes
- All payment data is encrypted and secure
- Receipt images are stored in the existing file storage system