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

31 KiB

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

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

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

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

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

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

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

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

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

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

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:

// 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