<?php

namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Libraries\InvoiceLedgerService;
use App\Models\DiscountVoucherModel;
use App\Models\DiscountUsageModel;
use App\Models\InvoiceModel;
use App\Models\UserModel;
use App\Models\ConfigurationModel;
use App\Models\PaymentModel;
use App\Models\EnrollmentModel;
use App\Models\EventChargesModel;
use App\Models\AdditionalChargeModel;
use App\Models\ClassSectionModel;
use CodeIgniter\Events\Events;

class DiscountController extends BaseController
{
    protected $configModel;
    protected $voucherModel;
    protected $invoiceModel;
    protected $userModel;
    protected $db;
    protected $schoolYear;
    protected $semester;
    protected $paymentModel;
    protected $enrollmentModel;
    protected $eventChargesModel;
    protected $additionalChargeModel;
    protected $classSectionModel;
    protected $invoiceLedgerService;

    public function __construct()
    {
        $this->configModel = new ConfigurationModel();
        $this->voucherModel = new DiscountVoucherModel();
        $this->invoiceModel = new InvoiceModel();
        $this->userModel = new UserModel();
        $this->db = \Config\Database::connect();
        $this->paymentModel = new PaymentModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->eventChargesModel = new EventChargesModel();
        $this->additionalChargeModel = new AdditionalChargeModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->invoiceLedgerService = new InvoiceLedgerService();

        $this->schoolYear = $this->configModel->getConfig('school_year');
        $this->semester = $this->configModel->getConfig('semester');
    }

    public function applyVoucher()
    {
        if (strtolower($this->request->getMethod()) === 'post') {
            $voucherId = $this->request->getPost('voucher_id');
            $parentIds = $this->request->getPost('parent_ids') ?? [];
            $allowAdditional = (bool) $this->request->getPost('allow_additional');

            if (empty($parentIds)) {
                return redirect()->back()->with('error', 'Please select at least one parent.');
            }

            $voucher = $this->voucherModel->find($voucherId);
            if (!$voucher) {
                return redirect()->back()->with('error', 'Voucher not found.');
            }

            // Normalize description from voucher (optional)
            $voucherDescription = trim((string) ($voucher['description'] ?? ''));
            $voucherDescription = preg_replace(
                '/^Auto-generated on \d{4}-\d{2}-\d{2}\s*[—-]\s*reason:\s*/i',
                '',
                $voucherDescription
            );

            $maxUsesRaw = $voucher['max_uses'] ?? null;
            $maxUses = ($maxUsesRaw === null || $maxUsesRaw === '') ? null : (int) $maxUsesRaw;
            $timesUsed = (int) ($voucher['times_used'] ?? 0);
            $remainingUses = ($maxUses === null) ? PHP_INT_MAX : ($maxUses - $timesUsed);
            if ($remainingUses <= 0) {
                return redirect()->back()->with('error', 'This voucher has reached its maximum allowed uses.');
            }

            // If not allowing additional discounts, filter out parents who already have discounts this school year.
            if (!$allowAdditional) {
                $rows = $this->db->table('discount_usages du')
                    ->select('i.parent_id')
                    ->join('invoices i', 'i.id = du.invoice_id')
                    ->whereIn('i.parent_id', $parentIds)
                    ->where('i.school_year', $this->schoolYear)
                    ->groupBy('i.parent_id')
                    ->get()
                    ->getResultArray();
                $blockedParentIds = array_map(static fn($r) => (int) $r['parent_id'], $rows);
                if (!empty($blockedParentIds)) {
                    $parentIds = array_values(array_diff(
                        array_map('intval', $parentIds),
                        $blockedParentIds
                    ));
                }
                if (empty($parentIds)) {
                    return redirect()->back()->with('error', 'All selected parents already have discounts. Enable "Allow additional discounts" to apply another.');
                }
            }

            $paymentDate = utc_now();

            // Collect invoice IDs that end up fully covered by the voucher in this run
            $fullyCoveredInvoiceIds = [];
            $touchedInvoiceIds = [];
            $appliedCount = 0;

            $this->db->transStart();

            foreach ($parentIds as $parentId) {
                // Fetch invoices for this parent & school year
                $invoices = $this->invoiceModel->getInvoicesByUserId($parentId, $this->schoolYear);
                if (empty($invoices)) {
                    // Do NOT early-return inside a transaction; just continue
                    session()->setFlashdata('error', 'A selected parent has no invoice in record.');
                    continue;
                }

                foreach ($invoices as $invoice) {
                    if ($remainingUses <= 0) break 2; // out of parentIds loop too

                    $this->db->query('SELECT id FROM invoices WHERE id = ? FOR UPDATE', [(int) $invoice['id']]);

                    // Snapshot current balance BEFORE applying
                    $initialPreBalance = (float) $this->getCurrentInvoiceBalance($invoice['id'], $this->schoolYear);
                    if ($initialPreBalance <= 0) {
                        log_message(
                            'error',
                            'applyVoucher skip: zero balance | voucher_id={vid} parent_id={pid} invoice_id={iid} invoice_number={inum} balance={bal}',
                            [
                                'vid'  => (int)$voucherId,
                                'pid'  => (int)$parentId,
                                'iid'  => (int)$invoice['id'],
                                'inum' => (string)($invoice['invoice_number'] ?? ''),
                                'bal'  => $initialPreBalance,
                            ]
                        );
                        continue;
                    }

                    // Already used this voucher on this invoice?
                    if (!$allowAdditional) {
                        $exists = $this->db->table('discount_usages du')
                            ->join('invoices i', 'du.invoice_id = i.id')
                            ->where('du.voucher_id', $voucherId)
                            ->where('du.invoice_id', $invoice['id'])
                            ->where('i.school_year', $this->schoolYear)
                            ->countAllResults();
                        if ($exists) {
                            log_message(
                                'error',
                                'applyVoucher skip: voucher already used | voucher_id={vid} parent_id={pid} invoice_id={iid} invoice_number={inum}',
                                [
                                    'vid'  => (int)$voucherId,
                                    'pid'  => (int)$parentId,
                                    'iid'  => (int)$invoice['id'],
                                    'inum' => (string)($invoice['invoice_number'] ?? ''),
                                ]
                            );
                            continue;
                        }
                    }

                    // Calculate discount
                    $rawDiscount = ($voucher['discount_type'] === 'percent')
                        ? round(((float)$invoice['total_amount'] * (float)$voucher['discount_value']) / 100, 2)
                        : (float) $voucher['discount_value'];

                    // Cap by CURRENT invoice balance snapshot
                    $discount = min($rawDiscount, $initialPreBalance);

                    // Nothing to do if no discount
                    if ($discount <= 0) {
                        log_message(
                            'error',
                            'applyVoucher skip: discount <= 0 | voucher_id={vid} parent_id={pid} invoice_id={iid} invoice_number={inum} raw_discount={raw} balance={bal}',
                            [
                                'vid'  => (int)$voucherId,
                                'pid'  => (int)$parentId,
                                'iid'  => (int)$invoice['id'],
                                'inum' => (string)($invoice['invoice_number'] ?? ''),
                                'raw'  => $rawDiscount,
                                'bal'  => $initialPreBalance,
                            ]
                        );
                        continue;
                    }

                    // Insert discount usage
                    $now = utc_now();
                    $this->db->table('discount_usages')->insert([
                        'voucher_id'      => $voucherId,
                        'invoice_id'      => $invoice['id'],
                        'parent_id'       => $parentId,
                        'discount_amount' => $discount,
                        'description'     => $voucherDescription,
                        'school_year'     => $this->schoolYear,
                        'updated_by'      => session()->get('user_id'),
                        'used_at'         => $now,
                        'created_at'      => $now,
                        'updated_at'      => $now,
                    ]);

                    $ledger = $this->invoiceLedgerService->recalculateInvoice((int) $invoice['id']);
                    $postBalance = (float) ($ledger['balance'] ?? 0.0);
                    $currentBalance = $postBalance;

                    // Increment voucher usage
                    $this->db->table('discount_vouchers')
                        ->where('id', $voucherId)
                        ->set('times_used', 'COALESCE(times_used,0) + 1', false)
                        ->update();

                    // Prepare and trigger payment event
                    [$eventData, $studentData] = $this->buildPaymentEventData(
                        $invoice['id'],
                        $voucherId,
                        $discount,
                        'discount',
                        $paymentDate,
                        '',
                        0,
                        $initialPreBalance,   // pre-payment snapshot
                        $postBalance          // computed post-payment
                    );
                    Events::trigger('paymentReceived', $eventData, $studentData);

                    $touchedInvoiceIds[] = (int) $invoice['id'];
                    $appliedCount++;

                    // If voucher covered the entire current balance, mark to update enrollments
                    // Use a small epsilon to tolerate cents rounding.
                    $epsilon = 0.01;
                    $fullyCovered = (abs($discount - $initialPreBalance) <= $epsilon)
                        || ($postBalance <= $epsilon)
                        || ($currentBalance <= $epsilon);

                    if ($fullyCovered) {
                        $fullyCoveredInvoiceIds[] = (int) $invoice['id'];
                    }

                    $remainingUses--;

                    // Deactivate if we just hit the cap
                    if ($remainingUses <= 0 && $maxUses !== null) {
                        $this->db->table('discount_vouchers')
                            ->where('id', $voucherId)
                            ->update([
                                'is_active'  => 0,
                                'updated_at' => $now,
                            ]);
                        break 2;
                    }
                }
            }

            $this->db->transComplete();

            if ($this->db->transStatus() === false) {
                return redirect()->back()->with('error', 'Voucher application failed. Transaction rolled back.');
            }

            if ($appliedCount === 0) {
                return redirect()->back()->with('error', 'No discounts were applied. The voucher may already be used for these invoices or balances are zero.');
            }

            // ✅ Ensure voucher deactivates when max_uses reached (handles edge cases)
            if ($maxUses !== null) {
                $row = $this->db->table('discount_vouchers')
                    ->select('times_used')
                    ->where('id', $voucherId)
                    ->get()
                    ->getRowArray();
                $usedNow = (int) ($row['times_used'] ?? 0);
                if ($usedNow >= $maxUses) {
                    $this->db->table('discount_vouchers')
                        ->where('id', $voucherId)
                        ->update([
                            'is_active'  => 0,
                            'updated_at' => utc_now(),
                        ]);
                }
            }

            // ✅ AFTER COMMIT: recalculate invoice totals/balance/paid/discount/refund
            foreach (array_unique($touchedInvoiceIds) as $iid) {
                try {
                    $this->recalculateInvoice($iid, $this->schoolYear);
                } catch (\Throwable $e) {
                    log_message('error', 'recalculateInvoice failed for invoice {iid}: {err}', [
                        'iid' => $iid,
                        'err' => $e->getMessage(),
                    ]);
                }
            }

            // ✅ AFTER COMMIT: update enrollments for fully-covered invoices
            // Avoid nested transactions by doing this post-commit.
            $updatedTotal = 0;
            foreach (array_unique($fullyCoveredInvoiceIds) as $iid) {
                try {
                    $updatedTotal += (int) $this->updateEnrollmentStatusIfPaid($iid);
                } catch (\Throwable $e) {
                    log_message('error', 'updateEnrollmentStatusIfPaid failed for invoice {iid}: {err}', [
                        'iid' => $iid,
                        'err' => $e->getMessage(),
                    ]);
                }
            }

            if ($updatedTotal > 0) {
                return redirect()->back()->with('success', 'Voucher applied successfully and enrollments updated.');
            }
            return redirect()->back()->with('success', 'Voucher applied successfully.');
        }

        // GET: load page
        $parents = $this->userModel->getParents();

        foreach ($parents as &$parent) {
            $discount = $this->db->table('discount_usages')
                ->select('COALESCE(SUM(discount_amount),0) AS total_discount')
                ->where('parent_id', $parent['id'])
                ->where('school_year', $this->schoolYear)
                ->get()
                ->getRowArray();

            $parent['total_discount'] = $discount['total_discount'] ?? 0;
            $parent['has_discount']   = ($parent['total_discount'] > 0) ? 1 : 0;
        }
        unset($parent);

        return view('discounts/apply_voucher', [
            'vouchers' => $this->voucherModel->where('is_active', 1)->findAll(),
            'parents'  => $parents,
        ]);
    }

    private function updateEnrollmentStatusIfPaid(int $invoiceId): int
    {
        // 1) Fetch invoice
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) {
            log_message('warning', 'Invoice not found: {id}', ['id' => $invoiceId]);
            return 0;
        }

        // 2) Payment check (any payment recorded: balance < total)
        $total   = (float) ($invoice['total_amount'] ?? 0);
        $balance = (float) ($invoice['balance'] ?? 0);
        if (!($total > 0 && $balance < $total)) {
            log_message('info', 'No payment yet. Skipping enrollment update. Invoice #{id}', ['id' => $invoiceId]);
            return 0;
        }

        $parentId   = (int) ($invoice['parent_id'] ?? 0);
        $schoolYear = (string) $this->schoolYear;
        if ($parentId <= 0 || $schoolYear === '') {
            log_message('warning', 'updateEnrollmentStatusIfPaid: missing parent_id/school_year for invoice #{id}', ['id' => $invoiceId]);
            return 0;
        }

        // 3) Try to limit to enrollments actually present on the invoice (optional)
        //    If invoice_items.enrollment_id doesn't exist, this silently falls back to parent/year scope.
        $paidEnrollmentIds = [];
        try {
            $rows = $this->db->table('invoice_items')
                ->select('enrollment_id')
                ->where('invoice_id', $invoiceId)
                ->where('enrollment_id IS NOT NULL', null, false)
                ->get()->getResultArray();
            foreach ($rows as $r) {
                $eid = (int) ($r['enrollment_id'] ?? 0);
                if ($eid > 0) $paidEnrollmentIds[] = $eid;
            }
            $paidEnrollmentIds = array_values(array_unique($paidEnrollmentIds));
        } catch (\Throwable $e) {
            // No-op: table/column might not exist in your schema
        }

        $db = $this->db;
        $db->transBegin();

        try {
            // 4) Preview IDs that will be updated (good for debugging “partials”)
            $previewBuilder = $db->table('enrollments')
                ->select('id')
                ->where('parent_id', $parentId)
                ->where('school_year', $schoolYear)
                ->groupStart()
                // tolerant match for 'payment pending' (case/space/nbsp)
                ->where('enrollment_status', 'payment pending')
                ->orWhere('enrollment_status', 'Payment pending')
                ->orWhere('enrollment_status', 'Payment Pending')
                ->orWhere('enrollment_status', 'PAYMENT PENDING')
                ->orWhere("LOWER(TRIM(REPLACE(enrollment_status, CHAR(160), ' '))) = 'payment pending'", null, false)
                ->groupEnd();

            if (!empty($paidEnrollmentIds)) {
                $previewBuilder->whereIn('id', $paidEnrollmentIds);
            }

            $toUpdateIds = array_map(
                static fn($r) => (int)$r['id'],
                $previewBuilder->get()->getResultArray()
            );

            if (empty($toUpdateIds)) {
                $db->transCommit();
                log_message('info', 'No enrollments matched pending status for parent {p}, year {y}, sem {s}.', [
                    'p' => $parentId,
                    'y' => $schoolYear,
                    's' => 'ALL'
                ]);
                return 0;
            }

            // 5) Perform update with same filters
            $builder = $db->table('enrollments');
            $builder->set('enrollment_status', 'enrolled')
                // Use DB date if you prefer: ->set('enrollment_date', 'CURRENT_DATE()', false)
                ->set('enrollment_date', local_date(utc_now(), 'Y-m-d'))
                ->whereIn('id', $toUpdateIds);

            if ($builder->update() === false) {
                $err = $db->error();
                throw new \RuntimeException('Enrollments update failed: ' . ($err['message'] ?? 'unknown DB error'));
            }

            $affected = $db->affectedRows();
            $db->transCommit();

            log_message(
                'info',
                'Enrollment status -> enrolled for {n} row(s). parent={p}, year={y}, sem={s}, ids=[{ids}]',
                [
                    'n'   => $affected,
                    'p'   => $parentId,
                    'y'   => $schoolYear,
                    's'   => 'ALL',
                    'ids' => implode(',', $toUpdateIds),
                ]
            );

            return $affected;
        } catch (\Throwable $e) {
            $db->transRollback();
            log_message('error', 'updateEnrollmentStatusIfPaid error: ' . $e->getMessage());
            return 0;
        }
    }


    public function listVouchers()
    {

        $vouchers = $this->voucherModel->findAll();
        return view('discounts/list', ['vouchers' => $vouchers]);
    }

    public function createVoucher()
    {
        if (strtolower($this->request->getMethod()) === 'post') {

            // -------- Gather & normalize inputs --------
            $rawCode        = (string) $this->request->getPost('code');
            // Keep A–Z, 0–9 and dashes; uppercase for consistency
            $code           = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($rawCode)));

            $discountType   = strtolower((string) $this->request->getPost('discount_type')); // 'percent' | 'fixed'
            $discountValue  = (float) ($this->request->getPost('discount_value') ?? 0);
            $maxUsesRaw     = $this->request->getPost('max_uses');
            $maxUses        = ($maxUsesRaw === '' || $maxUsesRaw === null) ? null : max(0, (int) $maxUsesRaw);

            $validFrom      = trim((string) ($this->request->getPost('valid_from') ?? ''));
            $validUntil     = trim((string) ($this->request->getPost('valid_until') ?? ''));
            $isActive       = $this->request->getPost('is_active') ? 1 : 0;

            // NEW: Description / Reason
            $description    = trim((string) ($this->request->getPost('description') ?? ''));

            // -------- Basic validations (before model rules) --------
            $errors = [];

            if ($code === '') {
                $errors[] = 'Voucher code is required.';
            }

            if (!in_array($discountType, ['percent', 'fixed'], true)) {
                $errors[] = 'Discount type must be "percent" or "fixed".';
            }

            if ($discountType === 'percent') {
                if ($discountValue <= 0 || $discountValue > 100) {
                    $errors[] = 'Percentage discount must be between 0 and 100.';
                }
            } else { // fixed
                if ($discountValue < 0) {
                    $errors[] = 'Fixed discount must be 0 or greater.';
                }
            }

            // Dates come from <input type="date"> as YYYY-MM-DD
            $validFrom = $validFrom !== '' ? $validFrom : null;
            $validUntil = $validUntil !== '' ? $validUntil : null;

            // Ensure date format is plausible
            $isDate = static function (?string $d): bool {
                if ($d === null) return true;
                return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
            };
            if (!$isDate($validFrom))  $errors[] = 'Valid From must be a date (YYYY-MM-DD).';
            if (!$isDate($validUntil)) $errors[] = 'Valid Until must be a date (YYYY-MM-DD).';

            if ($validFrom && $validUntil && $validFrom > $validUntil) {
                $errors[] = 'Valid Until must be the same as or after Valid From.';
            }

            // Optional: require a reason when auto-generated codes are used
            // if ($description === '' && strlen($code) === 10) {
            //     $errors[] = 'Please add a description/reason for this voucher.';
            // }

            if (!empty($errors)) {
                return redirect()->back()->withInput()->with('error', implode(' ', $errors));
            }

            // -------- Prepare payload for model --------
            $data = [
                'code'           => $code,
                'discount_type'  => $discountType,
                'discount_value' => $discountValue,
                'max_uses'       => $maxUses,
                'valid_from'     => $validFrom,
                'valid_until'    => $validUntil,
                'is_active'      => $isActive,
                'description'    => $description,   // <-- NEW
            ];

            if ($this->voucherModel->save($data)) {
                return redirect()->to('/discounts/list')->with('success', 'Voucher created successfully.');
            }

            // Model-level errors (unique code, etc.)
            return redirect()->back()
                ->withInput()
                ->with('error', 'Failed to save voucher: ' . implode('; ', (array) $this->voucherModel->errors()));
        }

        return view('discounts/create');
    }


    public function editVoucher($id)
    {
        $voucher = $this->voucherModel->find($id);

        if (!$voucher) {
            return redirect()->to('discounts/list')->with('error', 'Voucher not found');
        }

        if (strtolower($this->request->getMethod()) === 'post') {
            $data = [
                'id' => $id,
                'code' => $this->request->getPost('code'),
                'discount_type' => $this->request->getPost('discount_type'),
                'discount_value' => $this->request->getPost('discount_value'),
                'max_uses' => $this->request->getPost('max_uses'),
                'valid_from' => $this->request->getPost('valid_from') ?: null,
                'valid_until' => $this->request->getPost('valid_until') ?: null,
                'is_active' => $this->request->getPost('is_active') ? 1 : 0,
            ];

            $this->voucherModel->save($data);
            return redirect()->to('discounts/list')->with('success', 'Voucher updated successfully');
        }

        return view('discounts/edit', ['voucher' => $voucher]);
    }

    /**
     * 🔄 Helper: Current invoice balance (school-year scoped) = total - payments - discounts - refundsPaid
     */
    private function getCurrentInvoiceBalance($invoiceId, $schoolYear)
    {
        try {
            return (float) ($this->invoiceLedgerService->calculateInvoice((int) $invoiceId)['balance'] ?? 0.0);
        } catch (\Throwable $e) {
            return 0.0;
        }
    }

    /**
     * 🔄 Recalculate invoice totals and status based on all payments for current school year
     */
    private function recalculateInvoice($invoiceId, $schoolYear): void
    {
        $this->invoiceLedgerService->recalculateInvoice((int) $invoiceId);
    }

    /**
     * Parse a grade name into an integer level for tuition rules.
     * Kindergarten -> 1, Youth -> > 9, numeric grades passthrough.
     */
    private function parseGradeLevel($grade): int
    {
        if (is_numeric($grade)) return (int)$grade;
        if (!is_string($grade)) return 999;
        $g = strtoupper(trim((string)$grade));
        $g = preg_replace('/\s+/', ' ', $g);
        $g = str_replace(['.', '_', '-'], ['', '', ' '], $g);
        // KG/K/Kindergarten
        $kg = ['K','KG','K G','KINDER','KINDERGARTEN'];
        if (in_array($g, $kg, true)) return 1;
        // Pre-K -> treat below regular
        $pk = ['PK','P K','PREK','PRE K','PRE KINDER','PREKINDER'];
        if (in_array($g, $pk, true)) return -1;
        // Youth variants: Y, YOUTH, Y1, YOUTH2 -> map to 10+
        if (preg_match('/^Y(?:OUTH)?\s*(\d+)?$/', $g, $m)) {
            $n = isset($m[1]) && $m[1] !== '' ? max(1, (int)$m[1]) : 1;
            $gradeFee = (int)($this->configModel->getConfig('grade_fee') ?? 9);
            return $gradeFee + $n;
        }
        if (preg_match('/^(?:GR?ADE\s*)?(\d{1,2})\s*([A-Z]*)$/', $g, $m)) {
            return (int)$m[1];
        }
        return 999;
    }

    /**
     * Collect parent/invoice/payment data to trigger handlePaymentReceived().
     *
     * @return array [$data, $studentdata]
     */
    private function buildPaymentEventData(
        int $invoiceId,
        string $transactionIdOrRef,
        float $amount,
        string $paymentMethod,
        string $paymentDate,
        ?string $checkNumber,
        int $installmentSeq,
        float $preBalance,     // ✅ new param: initial (pre-payment) balance
        float $postBalance     // ✅ new param: computed post-payment balance
    ): array {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) {
            throw new \RuntimeException("Invoice {$invoiceId} not found");
        }

        $parentRow = $this->db->table('users')
            ->select('id, firstname, lastname, email')
            ->where('id', $invoice['parent_id'])
            ->get()
            ->getRowArray();

        if (!$parentRow) {
            throw new \RuntimeException("Parent not found for invoice {$invoiceId}");
        }

        // Students (adjust joins to your schema)
        $studentRows = $this->db->table('students s')
            ->select('s.id, s.firstname, s.lastname, sc.class_section_id')
            ->join('student_class sc', 'sc.student_id = s.id', 'left')
            ->where('s.parent_id', $invoice['parent_id'])
            ->where('sc.school_year', $this->schoolYear)
            ->get()
            ->getResultArray();

        $data = [
            'user_id'         => (int) $parentRow['id'],
            'email'           => $parentRow['email'],
            'firstname'       => $parentRow['firstname'],
            'lastname'        => $parentRow['lastname'],
            'school_year'     => $this->schoolYear,
            'semester'        => $this->semester,
            'invoice_id'      => $invoiceId,
            'transaction_id'  => $transactionIdOrRef,
            'payment_date'    => $paymentDate,
            'amount'          => $amount,
            'method'          => $paymentMethod,
            'check_number'    => $checkNumber,
            'installment_seq' => $installmentSeq,
            'invoice_total'   => (float) $invoice['total_amount'],
            'pre_balance'     => $preBalance,   // ✅ the captured pre-payment balance
            'post_balance'    => $postBalance,  // ✅ computed post-payment balance
            'portalLink'      => site_url('parent/invoices/' . $invoiceId),
        ];

        return [$data, $studentRows];
    }
}
