Skip to main content
Learn how to implement a complete school fee collection system using Yabetoo. This guide covers tuition payments, installment plans, multiple fee types, and parent notifications.

Overview

Educational institutions need payment solutions to handle:
  • Tuition fee collection
  • Registration and enrollment fees
  • Exam and certification fees
  • Installment payment plans
  • Multiple students per family
  • Payment receipts and reporting

Architecture

Implementation

1. Define Fee Structure

Set up your school’s fee categories:
// fee-structure.ts
export interface FeeType {
  id: string;
  name: string;
  amount: number;
  category: 'tuition' | 'registration' | 'exam' | 'materials' | 'other';
  mandatory: boolean;
  academicYear: string;
}

export const FEE_STRUCTURE: Record<string, FeeType[]> = {
  'primary': [
    { id: 'tuition-primary', name: 'Tuition Fee', amount: 150000, category: 'tuition', mandatory: true, academicYear: '2024-2025' },
    { id: 'registration', name: 'Registration Fee', amount: 25000, category: 'registration', mandatory: true, academicYear: '2024-2025' },
    { id: 'exam-fee', name: 'Exam Fee', amount: 15000, category: 'exam', mandatory: true, academicYear: '2024-2025' },
    { id: 'materials', name: 'School Materials', amount: 35000, category: 'materials', mandatory: false, academicYear: '2024-2025' }
  ],
  'secondary': [
    { id: 'tuition-secondary', name: 'Tuition Fee', amount: 250000, category: 'tuition', mandatory: true, academicYear: '2024-2025' },
    { id: 'registration', name: 'Registration Fee', amount: 35000, category: 'registration', mandatory: true, academicYear: '2024-2025' },
    { id: 'exam-fee', name: 'Exam Fee', amount: 25000, category: 'exam', mandatory: true, academicYear: '2024-2025' },
    { id: 'lab-fee', name: 'Laboratory Fee', amount: 20000, category: 'materials', mandatory: true, academicYear: '2024-2025' }
  ]
};

2. Student Fee Management

Create a system to track student fees:
// student-fees.ts
import Yabetoo from '@yabetoo/sdk-js';

const yabetoo = new Yabetoo(process.env.YABETOO_SECRET_KEY!);

interface StudentFeeAccount {
  studentId: string;
  studentName: string;
  class: string;
  level: 'primary' | 'secondary';
  academicYear: string;
  totalDue: number;
  totalPaid: number;
  balance: number;
  payments: Payment[];
}

async function getStudentFeeAccount(studentId: string): Promise<StudentFeeAccount> {
  const student = await db.students.findById(studentId);
  const fees = FEE_STRUCTURE[student.level];
  const payments = await db.payments.findByStudent(studentId);

  const totalDue = fees
    .filter(f => f.mandatory)
    .reduce((sum, fee) => sum + fee.amount, 0);

  const totalPaid = payments
    .filter(p => p.status === 'succeeded')
    .reduce((sum, p) => sum + p.amount, 0);

  return {
    studentId: student.id,
    studentName: `${student.firstName} ${student.lastName}`,
    class: student.class,
    level: student.level,
    academicYear: student.academicYear,
    totalDue,
    totalPaid,
    balance: totalDue - totalPaid,
    payments
  };
}

3. Create Fee Payment

Allow parents to pay fees:
async function createFeePayment(
  studentId: string,
  feeIds: string[],
  parentInfo: { firstName: string; lastName: string; phone: string; email?: string }
) {
  const student = await db.students.findById(studentId);
  const fees = FEE_STRUCTURE[student.level].filter(f => feeIds.includes(f.id));

  const totalAmount = fees.reduce((sum, fee) => sum + fee.amount, 0);

  const intent = await yabetoo.payments.create({
    amount: totalAmount,
    currency: 'XAF',
    description: `School fees for ${student.firstName} ${student.lastName}`,
    metadata: {
      studentId,
      studentName: `${student.firstName} ${student.lastName}`,
      class: student.class,
      academicYear: student.academicYear,
      feeIds: feeIds.join(','),
      feeDetails: JSON.stringify(fees.map(f => ({ id: f.id, name: f.name, amount: f.amount }))),
      parentPhone: parentInfo.phone,
      parentEmail: parentInfo.email,
      type: 'school_fee'
    }
  });

  // Store pending payment
  await db.pendingPayments.create({
    paymentIntentId: intent.id,
    studentId,
    feeIds,
    amount: totalAmount,
    parentInfo
  });

  return intent;
}

4. Installment Payment Plans

Support payment in installments:
interface InstallmentPlan {
  id: string;
  name: string;
  installments: number;
  schedule: { dueDate: Date; percentage: number }[];
}

const INSTALLMENT_PLANS: InstallmentPlan[] = [
  {
    id: 'full',
    name: 'Full Payment',
    installments: 1,
    schedule: [{ dueDate: new Date('2024-09-01'), percentage: 100 }]
  },
  {
    id: 'trimester',
    name: 'Trimester Plan (3 payments)',
    installments: 3,
    schedule: [
      { dueDate: new Date('2024-09-01'), percentage: 40 },
      { dueDate: new Date('2024-12-01'), percentage: 30 },
      { dueDate: new Date('2025-03-01'), percentage: 30 }
    ]
  },
  {
    id: 'monthly',
    name: 'Monthly Plan (10 payments)',
    installments: 10,
    schedule: Array.from({ length: 10 }, (_, i) => ({
      dueDate: new Date(2024, 8 + i, 1), // September to June
      percentage: 10
    }))
  }
];

async function createInstallmentPlan(
  studentId: string,
  planId: string,
  totalAmount: number
) {
  const plan = INSTALLMENT_PLANS.find(p => p.id === planId);
  if (!plan) throw new Error('Invalid installment plan');

  const installments = plan.schedule.map((schedule, index) => ({
    studentId,
    installmentNumber: index + 1,
    totalInstallments: plan.installments,
    amount: Math.round(totalAmount * (schedule.percentage / 100)),
    dueDate: schedule.dueDate,
    status: 'pending'
  }));

  // Save installment plan to database
  const savedPlan = await db.installmentPlans.create({
    studentId,
    planId,
    totalAmount,
    installments
  });

  return savedPlan;
}

async function payInstallment(installmentId: string, parentInfo: any) {
  const installment = await db.installments.findById(installmentId);
  const student = await db.students.findById(installment.studentId);

  const intent = await yabetoo.payments.create({
    amount: installment.amount,
    currency: 'XAF',
    description: `Installment ${installment.installmentNumber}/${installment.totalInstallments} - ${student.firstName} ${student.lastName}`,
    metadata: {
      studentId: installment.studentId,
      installmentId,
      installmentNumber: installment.installmentNumber,
      type: 'installment'
    }
  });

  return intent;
}

5. Webhook Handler

Process payment confirmations:
app.post('/webhooks/yabetoo', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment_intent.succeeded') {
    const { metadata, amount } = event.data;

    if (metadata.type === 'school_fee') {
      // Record payment
      await db.payments.create({
        studentId: metadata.studentId,
        amount,
        paymentIntentId: event.data.id,
        feeIds: metadata.feeIds.split(','),
        status: 'succeeded',
        paidAt: new Date()
      });

      // Update student account
      await updateStudentBalance(metadata.studentId);

      // Generate and send receipt
      const receipt = await generateReceipt(event.data);
      await sendReceiptByEmail(metadata.parentEmail, receipt);
      await sendReceiptBySMS(metadata.parentPhone, receipt.summary);

      // Notify school administration
      await notifyAdministration(metadata.studentId, amount);
    }

    if (metadata.type === 'installment') {
      await db.installments.update(metadata.installmentId, {
        status: 'paid',
        paidAt: new Date(),
        paymentIntentId: event.data.id
      });

      // Check if all installments are paid
      await checkInstallmentPlanCompletion(metadata.studentId);
    }
  }

  res.json({ received: true });
});

6. Receipt Generation

Generate official payment receipts:
interface PaymentReceipt {
  receiptNumber: string;
  date: Date;
  studentName: string;
  studentId: string;
  class: string;
  academicYear: string;
  fees: { name: string; amount: number }[];
  totalAmount: number;
  paymentMethod: string;
  transactionId: string;
}

async function generateReceipt(paymentData: any): Promise<PaymentReceipt> {
  const receiptNumber = `REC-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`;

  const receipt: PaymentReceipt = {
    receiptNumber,
    date: new Date(),
    studentName: paymentData.metadata.studentName,
    studentId: paymentData.metadata.studentId,
    class: paymentData.metadata.class,
    academicYear: paymentData.metadata.academicYear,
    fees: JSON.parse(paymentData.metadata.feeDetails),
    totalAmount: paymentData.amount,
    paymentMethod: 'Mobile Money',
    transactionId: paymentData.id
  };

  // Save receipt to database
  await db.receipts.create(receipt);

  return receipt;
}

7. Reporting Dashboard

Track fee collection progress:
async function getFeeCollectionReport(academicYear: string) {
  const students = await db.students.findByAcademicYear(academicYear);

  const report = {
    academicYear,
    totalStudents: students.length,
    totalExpected: 0,
    totalCollected: 0,
    collectionRate: 0,
    byClass: {} as Record<string, { expected: number; collected: number; students: number }>,
    overdueAccounts: [] as any[]
  };

  for (const student of students) {
    const account = await getStudentFeeAccount(student.id);

    report.totalExpected += account.totalDue;
    report.totalCollected += account.totalPaid;

    // Group by class
    if (!report.byClass[student.class]) {
      report.byClass[student.class] = { expected: 0, collected: 0, students: 0 };
    }
    report.byClass[student.class].expected += account.totalDue;
    report.byClass[student.class].collected += account.totalPaid;
    report.byClass[student.class].students += 1;

    // Track overdue accounts
    if (account.balance > 0) {
      report.overdueAccounts.push({
        studentId: student.id,
        studentName: account.studentName,
        class: student.class,
        balance: account.balance
      });
    }
  }

  report.collectionRate = (report.totalCollected / report.totalExpected) * 100;

  return report;
}

Payment Flow

1

Parent Accesses Portal

Parent logs into the school portal and views their child’s fee account.
2

Select Fees to Pay

Parent selects which fees to pay (full amount or specific items).
3

Choose Payment Plan

Parent selects full payment or installment plan if available.
4

Enter Payment Details

Parent enters their mobile money number (MTN or Airtel).
5

Confirm Payment

Parent confirms the payment on their mobile phone.
6

Receive Receipt

Parent receives an official receipt via email and SMS.
7

Account Updated

Student’s fee account is updated automatically.

Best Practices

Send automated reminders before installment due dates (7 days, 3 days, 1 day before).
Implement sibling discounts for families with multiple children enrolled.
Consider grace periods before applying late payment penalties.
Support scholarship and financial aid deductions from total fees.

SMS Notification Templates

const SMS_TEMPLATES = {
  paymentReceived: (data: any) =>
    `School Fee Payment Received. Amount: ${data.amount} XAF for ${data.studentName}. Receipt: ${data.receiptNumber}. Balance: ${data.balance} XAF.`,

  installmentReminder: (data: any) =>
    `Reminder: Installment ${data.number} of ${data.total} (${data.amount} XAF) for ${data.studentName} is due on ${data.dueDate}. Pay via school portal.`,

  overdueNotice: (data: any) =>
    `Notice: Outstanding balance of ${data.balance} XAF for ${data.studentName}. Please settle to avoid service interruption.`
};