<?php
/*
 * /edenrms/includes/finance_engine.php
 * Central finance helper for EDEN RMS.
 * Safe to include from pages, ajax, popups. No output.
 */
declare(strict_types=1);

if (!function_exists('fe_table_exists')) {
    function fe_table_exists(mysqli $db, string $table): bool {
        static $cache = [];
        $key = spl_object_id($db) . ':' . $table;
        if (array_key_exists($key, $cache)) return $cache[$key];
        $st = $db->prepare("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1");
        if (!$st) return $cache[$key] = false;
        $st->bind_param('s', $table);
        $st->execute();
        $st->store_result();
        $ok = $st->num_rows > 0;
        $st->free_result();
        $st->close();
        return $cache[$key] = $ok;
    }
}

if (!function_exists('fe_col_exists')) {
    function fe_col_exists(mysqli $db, string $table, string $column): bool {
        static $cache = [];
        $key = spl_object_id($db) . ':' . $table . ':' . $column;
        if (array_key_exists($key, $cache)) return $cache[$key];
        $st = $db->prepare("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1");
        if (!$st) return $cache[$key] = false;
        $st->bind_param('ss', $table, $column);
        $st->execute();
        $st->store_result();
        $ok = $st->num_rows > 0;
        $st->free_result();
        $st->close();
        return $cache[$key] = $ok;
    }
}

if (!function_exists('fe_first_col')) {
    function fe_first_col(mysqli $db, string $table, array $cols, ?string $fallback = null): ?string {
        foreach ($cols as $c) {
            if (fe_col_exists($db, $table, $c)) return $c;
        }
        return $fallback;
    }
}

if (!function_exists('fe_num')) {
    function fe_num($value): float {
        if (is_int($value) || is_float($value)) return round((float)$value, 2);
        $s = preg_replace('/[^0-9.\-]/', '', (string)$value);
        if ($s === '' || $s === '-' || $s === '.' || $s === '-.') return 0.0;
        $n = (float)$s;
        return is_finite($n) ? round($n, 2) : 0.0;
    }
}

if (!function_exists('fe_fetch_one')) {
    function fe_fetch_one(mysqli $db, string $sql, string $types = '', array $params = []): ?array {
        $st = $db->prepare($sql);
        if (!$st) return null;
        if ($types !== '' && $params) $st->bind_param($types, ...$params);
        if (!$st->execute()) { $st->close(); return null; }
        $rs = $st->get_result();
        $row = $rs ? $rs->fetch_assoc() : null;
        $st->close();
        return $row ?: null;
    }
}

if (!function_exists('fe_fetch_all')) {
    function fe_fetch_all(mysqli $db, string $sql, string $types = '', array $params = []): array {
        $st = $db->prepare($sql);
        if (!$st) return [];
        if ($types !== '' && $params) $st->bind_param($types, ...$params);
        if (!$st->execute()) { $st->close(); return []; }
        $rs = $st->get_result();
        $out = [];
        while ($rs && ($row = $rs->fetch_assoc())) $out[] = $row;
        $st->close();
        return $out;
    }
}

if (!function_exists('finance_get_student')) {
    function finance_get_student(mysqli $db, int $studentId): ?array {
        if ($studentId <= 0 || !fe_table_exists($db, 'studentss')) return null;
        return fe_fetch_one($db, "SELECT * FROM studentss WHERE id=? LIMIT 1", 'i', [$studentId]);
    }
}

if (!function_exists('finance_get_active_plan')) {
    function finance_get_active_plan(mysqli $db, int $studentId, int $courseId = 0, int $planId = 0): ?array {
        if ($studentId <= 0 || !fe_table_exists($db, 'student_fee_plans')) return null;
        if (!fe_col_exists($db, 'student_fee_plans', 'student_id')) return null;

        if ($planId > 0) {
            $sql = "SELECT * FROM student_fee_plans WHERE id=?";
            $types = 'i'; $params = [$planId];
            if (fe_col_exists($db, 'student_fee_plans', 'student_id')) { $sql .= " AND student_id=?"; $types .= 'i'; $params[] = $studentId; }
            if ($courseId > 0 && fe_col_exists($db, 'student_fee_plans', 'course_id')) { $sql .= " AND course_id=?"; $types .= 'i'; $params[] = $courseId; }
            $sql .= " LIMIT 1";
            $row = fe_fetch_one($db, $sql, $types, $params);
            if ($row) return $row;
        }

        $sql = "SELECT * FROM student_fee_plans WHERE student_id=?";
        $types = 'i'; $params = [$studentId];
        if ($courseId > 0 && fe_col_exists($db, 'student_fee_plans', 'course_id')) { $sql .= " AND course_id=?"; $types .= 'i'; $params[] = $courseId; }
        $sql .= " ORDER BY id DESC LIMIT 1";
        return fe_fetch_one($db, $sql, $types, $params);
    }
}

if (!function_exists('finance_plan_type')) {
    function finance_plan_type(array $plan): string {
        $t = strtolower(trim((string)($plan['plan_type'] ?? $plan['type'] ?? '')));
        if ($t !== '') return $t;
        $n = strtolower(trim((string)($plan['plan_name'] ?? $plan['name'] ?? '')));
        if (str_contains($n, 'install')) return 'installment';
        if (str_contains($n, 'one')) return 'one';
        return 'one';
    }
}

if (!function_exists('finance_get_plan_amounts')) {
    function finance_get_plan_amounts(mysqli $db, array $plan): array {
        $courseFee = fe_num($plan['course_fee_lkr'] ?? $plan['course_fee'] ?? $plan['total_fee_lkr'] ?? $plan['total_fee'] ?? $plan['fee_lkr'] ?? 0);
        $regFee    = fe_num($plan['registration_fee_lkr'] ?? $plan['registration_fee'] ?? $plan['reg_fee_lkr'] ?? $plan['reg_fee'] ?? 0);
        $certUsd   = fe_num($plan['certificate_fee_usd'] ?? $plan['cert_fee_usd'] ?? $plan['uk_award_fee_usd'] ?? 0);
        $extraFee  = fe_num($plan['extra_fee_lkr'] ?? $plan['extra_fees_lkr'] ?? $plan['extra_fee'] ?? 0);
        $late      = fe_num($plan['late_charges_lkr'] ?? $plan['late_charge_lkr'] ?? $plan['late_charges'] ?? 0);

        $discount = 0.0;
        foreach (['plan_discount_lkr','manual_discount_lkr','method_discount_lkr','discount_lkr','discount_amount_lkr','discount_amount'] as $c) {
            if (array_key_exists($c, $plan)) $discount += fe_num($plan[$c]);
        }

        return [
            'course_fee_lkr' => $courseFee,
            'registration_fee_lkr' => $regFee,
            'certificate_fee_usd' => $certUsd,
            'extra_fee_lkr' => $extraFee,
            'late_charges_lkr' => $late,
            'discount_lkr' => $discount,
            'gross_lkr' => round($courseFee + $regFee + $extraFee + $late, 2),
            'net_lkr' => max(0, round($courseFee + $regFee + $extraFee + $late - $discount, 2)),
        ];
    }
}

if (!function_exists('finance_get_schedule_lines')) {
    function finance_get_schedule_lines(mysqli $db, int $studentId, int $planId = 0, int $courseId = 0): array {
        if ($studentId <= 0 || !fe_table_exists($db, 'payment_schedule')) return [];
        if (!fe_col_exists($db, 'payment_schedule', 'student_id')) return [];

        $sql = "SELECT * FROM payment_schedule WHERE student_id=?";
        $types = 'i'; $params = [$studentId];
        if ($planId > 0 && fe_col_exists($db, 'payment_schedule', 'plan_id')) { $sql .= " AND plan_id=?"; $types .= 'i'; $params[] = $planId; }
        if ($courseId > 0 && fe_col_exists($db, 'payment_schedule', 'course_id')) { $sql .= " AND course_id=?"; $types .= 'i'; $params[] = $courseId; }
        $dueCol = fe_first_col($db, 'payment_schedule', ['due_date','installment_due','due_on','pay_on'], 'id');
        $instCol = fe_first_col($db, 'payment_schedule', ['installment_no','installment','inst_no','seq'], 'id');
        $sql .= " ORDER BY `$dueCol` ASC, `$instCol` ASC, id ASC";
        $rows = fe_fetch_all($db, $sql, $types, $params);

        foreach ($rows as &$r) {
            $r['_line_amount_lkr'] = round(
                fe_num($r['amount_due'] ?? 0)
                ?: (fe_num($r['course_fee_lkr'] ?? $r['course_fee'] ?? 0) + fe_num($r['registration_fee_lkr'] ?? $r['registration_fee'] ?? $r['reg_fee'] ?? 0) + fe_num($r['extra_fee_lkr'] ?? 0) + fe_num($r['late_charge_lkr'] ?? 0)),
                2
            );
            $r['_cert_fee_usd'] = fe_num($r['certificate_fee_usd'] ?? $r['cert_fee_usd'] ?? 0);
            $r['_due_date'] = (string)($r['due_date'] ?? $r['installment_due'] ?? $r['due_on'] ?? $r['pay_on'] ?? '');
        }
        unset($r);
        return $rows;
    }
}

if (!function_exists('finance_get_payment_total')) {
    function finance_get_payment_total(mysqli $db, int $studentId, int $planId = 0, int $courseId = 0, int $scheduleId = 0): array {
        if ($studentId <= 0 || !fe_table_exists($db, 'payments') || !fe_col_exists($db, 'payments', 'student_id')) {
            return ['lkr'=>0.0, 'usd'=>0.0];
        }
        $amountCol = fe_first_col($db, 'payments', ['amount','paid_amount','amount_lkr','value']);
        if (!$amountCol) return ['lkr'=>0.0, 'usd'=>0.0];
        $usdCol = fe_first_col($db, 'payments', ['usd_amount','amount_usd']);

        $sql = "SELECT COALESCE(SUM(COALESCE(`$amountCol`,0)),0) AS lkr";
        $sql .= $usdCol ? ", COALESCE(SUM(COALESCE(`$usdCol`,0)),0) AS usd" : ", 0 AS usd";
        $sql .= " FROM payments WHERE student_id=?";
        $types = 'i'; $params = [$studentId];

        if ($scheduleId > 0 && fe_col_exists($db, 'payments', 'schedule_id')) { $sql .= " AND schedule_id=?"; $types .= 'i'; $params[] = $scheduleId; }
        if ($planId > 0 && fe_col_exists($db, 'payments', 'plan_id')) { $sql .= " AND plan_id=?"; $types .= 'i'; $params[] = $planId; }
        elseif ($courseId > 0 && fe_col_exists($db, 'payments', 'course_id')) { $sql .= " AND course_id=?"; $types .= 'i'; $params[] = $courseId; }

        // ignore cancelled/void payments if a status column exists
        $statusCol = fe_first_col($db, 'payments', ['status','state']);
        if ($statusCol) $sql .= " AND (LOWER(COALESCE(`$statusCol`,'')) NOT IN ('cancelled','canceled','void','deleted','rejected'))";

        $row = fe_fetch_one($db, $sql, $types, $params) ?: [];
        return ['lkr'=>fe_num($row['lkr'] ?? 0), 'usd'=>fe_num($row['usd'] ?? 0)];
    }
}

if (!function_exists('finance_get_paid_by_schedule')) {
    function finance_get_paid_by_schedule(mysqli $db, int $studentId, int $planId = 0, int $courseId = 0): array {
        $out = [];
        if (!fe_table_exists($db, 'payments') || !fe_col_exists($db, 'payments', 'schedule_id')) return $out;
        $amountCol = fe_first_col($db, 'payments', ['amount','paid_amount','amount_lkr','value']);
        if (!$amountCol) return $out;
        $sql = "SELECT schedule_id, COALESCE(SUM(COALESCE(`$amountCol`,0)),0) AS paid FROM payments WHERE student_id=? AND schedule_id IS NOT NULL AND schedule_id>0";
        $types = 'i'; $params = [$studentId];
        if ($planId > 0 && fe_col_exists($db, 'payments', 'plan_id')) { $sql .= " AND plan_id=?"; $types .= 'i'; $params[] = $planId; }
        elseif ($courseId > 0 && fe_col_exists($db, 'payments', 'course_id')) { $sql .= " AND course_id=?"; $types .= 'i'; $params[] = $courseId; }
        $statusCol = fe_first_col($db, 'payments', ['status','state']);
        if ($statusCol) $sql .= " AND (LOWER(COALESCE(`$statusCol`,'')) NOT IN ('cancelled','canceled','void','deleted','rejected'))";
        $sql .= " GROUP BY schedule_id";
        foreach (fe_fetch_all($db, $sql, $types, $params) as $r) $out[(int)$r['schedule_id']] = fe_num($r['paid'] ?? 0);
        return $out;
    }
}

if (!function_exists('finance_get_due_breakdown')) {
    function finance_get_due_breakdown(mysqli $db, int $studentId, int $courseId = 0, int $planId = 0, ?string $asOfDate = null): array {
        $asOfDate = $asOfDate ?: date('Y-m-d');
        $student = finance_get_student($db, $studentId);
        $plan = finance_get_active_plan($db, $studentId, $courseId, $planId);
        if (!$student || !$plan) {
            return ['ok'=>false, 'message'=>'Student or fee plan not found', 'student'=>$student, 'plan'=>$plan, 'lines'=>[], 'summary'=>[]];
        }

        $planId = (int)($plan['id'] ?? 0);
        $courseId = $courseId > 0 ? $courseId : (int)($plan['course_id'] ?? $student['course_id'] ?? 0);
        $amounts = finance_get_plan_amounts($db, $plan);
        $lines = finance_get_schedule_lines($db, $studentId, $planId, $courseId);
        $paid = finance_get_payment_total($db, $studentId, $planId, $courseId);
        $paidBySchedule = finance_get_paid_by_schedule($db, $studentId, $planId, $courseId);

        $totalLkr = $amounts['net_lkr'];
        $totalUsd = $amounts['certificate_fee_usd'];
        if ($lines) {
            $sumL = 0.0; $sumU = 0.0;
            foreach ($lines as $ln) { $sumL += fe_num($ln['_line_amount_lkr']); $sumU += fe_num($ln['_cert_fee_usd']); }
            if ($sumL > 0) $totalLkr = round(max(0, $sumL + $amounts['extra_fee_lkr'] + $amounts['late_charges_lkr'] - $amounts['discount_lkr']), 2);
            if ($sumU > 0) $totalUsd = round($sumU, 2);
        }

        $dueToDateLkr = 0.0;
        $overdueLkr = 0.0;
        $nextDueDate = null;
        $oldestDueDate = null;
        $lineOut = [];

        if ($lines) {
            foreach ($lines as $ln) {
                $sid = (int)($ln['id'] ?? 0);
                $due = (string)($ln['_due_date'] ?? '');
                $amt = fe_num($ln['_line_amount_lkr']);
                $linePaid = $paidBySchedule[$sid] ?? 0.0;
                if ($linePaid <= 0 && !fe_col_exists($db, 'payments', 'schedule_id')) {
                    // no schedule_id column: allocate total payments sequentially later
                    $linePaid = 0.0;
                }
                $bal = max(0, round($amt - $linePaid, 2));
                if ($due !== '') {
                    if ($oldestDueDate === null || $due < $oldestDueDate) $oldestDueDate = $due;
                    if ($bal > 0.009 && ($nextDueDate === null || $due < $nextDueDate)) $nextDueDate = $due;
                    if ($due <= $asOfDate) $dueToDateLkr += $amt;
                    if ($due < $asOfDate) $overdueLkr += $bal;
                }
                $ln['_paid_lkr'] = $linePaid;
                $ln['_balance_lkr'] = $bal;
                $lineOut[] = $ln;
            }

            if (!fe_col_exists($db, 'payments', 'schedule_id')) {
                $remaining = $paid['lkr'];
                $dueToDateLkr = 0.0; $overdueLkr = 0.0; $lineOut = [];
                foreach ($lines as $ln) {
                    $amt = fe_num($ln['_line_amount_lkr']);
                    $applied = min($amt, max(0, $remaining));
                    $remaining -= $applied;
                    $bal = max(0, round($amt - $applied, 2));
                    $due = (string)($ln['_due_date'] ?? '');
                    if ($due !== '') {
                        if ($due <= $asOfDate) $dueToDateLkr += $amt;
                        if ($due < $asOfDate) $overdueLkr += $bal;
                        if ($bal > 0.009 && ($nextDueDate === null || $due < $nextDueDate)) $nextDueDate = $due;
                    }
                    $ln['_paid_lkr'] = $applied;
                    $ln['_balance_lkr'] = $bal;
                    $lineOut[] = $ln;
                }
            }
        } else {
            // one-time/full fee plan: all due immediately by registration/created date
            $dueToDateLkr = $totalLkr;
            $overdueLkr = max(0, round($totalLkr - $paid['lkr'], 2));
        }

        $outstandingLkr = max(0, round($totalLkr - $paid['lkr'], 2));
        $outstandingUsd = max(0, round($totalUsd - $paid['usd'], 2));
        $dueBalanceLkr = max(0, round($dueToDateLkr - $paid['lkr'], 2));

        $status = 'Paid';
        if ($outstandingLkr <= 0.009 && $outstandingUsd <= 0.009) $status = 'Completed';
        elseif ($dueBalanceLkr > 0.009 || $overdueLkr > 0.009) $status = 'Overdue';

        // Inactive/suspended students should not show as financial active in lists
        foreach (['lms_status','student_status','status'] as $sc) {
            if (isset($student[$sc])) {
                $sv = strtolower(trim((string)$student[$sc]));
                if (in_array($sv, ['inactive','suspended','disabled'], true)) { $status = 'N/A'; break; }
            }
        }

        return [
            'ok'=>true,
            'student'=>$student,
            'plan'=>$plan,
            'course_id'=>$courseId,
            'plan_id'=>$planId,
            'plan_type'=>finance_plan_type($plan),
            'lines'=>$lineOut,
            'summary'=>[
                'total_lkr'=>$totalLkr,
                'total_usd'=>$totalUsd,
                'gross_lkr'=>$amounts['gross_lkr'],
                'discount_lkr'=>$amounts['discount_lkr'],
                'extra_fee_lkr'=>$amounts['extra_fee_lkr'],
                'late_charges_lkr'=>$amounts['late_charges_lkr'],
                'paid_lkr'=>$paid['lkr'],
                'paid_usd'=>$paid['usd'],
                'due_to_date_lkr'=>round($dueToDateLkr, 2),
                'due_balance_lkr'=>$dueBalanceLkr,
                'overdue_lkr'=>round($overdueLkr, 2),
                'outstanding_lkr'=>$outstandingLkr,
                'outstanding_usd'=>$outstandingUsd,
                'next_due_date'=>$nextDueDate,
                'oldest_due_date'=>$oldestDueDate,
                'status'=>$status,
            ],
        ];
    }
}

if (!function_exists('finance_get_student_summary')) {
    function finance_get_student_summary(mysqli $db, int $studentId, int $courseId = 0, int $planId = 0, ?string $asOfDate = null): array {
        return finance_get_due_breakdown($db, $studentId, $courseId, $planId, $asOfDate);
    }
}

if (!function_exists('finance_get_outstanding')) {
    function finance_get_outstanding(mysqli $db, int $studentId, int $courseId = 0, int $planId = 0): float {
        $x = finance_get_due_breakdown($db, $studentId, $courseId, $planId);
        return fe_num($x['summary']['outstanding_lkr'] ?? 0);
    }
}

if (!function_exists('finance_get_status')) {
    function finance_get_status(mysqli $db, int $studentId, int $courseId = 0, int $planId = 0): string {
        $x = finance_get_due_breakdown($db, $studentId, $courseId, $planId);
        return (string)($x['summary']['status'] ?? 'N/A');
    }
}

if (!function_exists('finance_next_unpaid_schedule_id')) {
    function finance_next_unpaid_schedule_id(mysqli $db, int $studentId, int $planId = 0, int $courseId = 0): ?int {
        $x = finance_get_due_breakdown($db, $studentId, $courseId, $planId);
        foreach (($x['lines'] ?? []) as $ln) {
            if (fe_num($ln['_balance_lkr'] ?? 0) > 0.009) return (int)$ln['id'];
        }
        if (!empty($x['lines'][0]['id'])) return (int)$x['lines'][0]['id'];
        return null;
    }
}

if (!function_exists('finance_resolve_course_id')) {
    function finance_resolve_course_id(mysqli $db, int $studentId, int $postedCourseId = 0, int $postedPlanId = 0): int {
        if ($postedCourseId > 0) return $postedCourseId;
        if ($postedPlanId > 0 && fe_table_exists($db, 'student_fee_plans') && fe_col_exists($db, 'student_fee_plans', 'course_id')) {
            $r = fe_fetch_one($db, "SELECT course_id FROM student_fee_plans WHERE id=? LIMIT 1", 'i', [$postedPlanId]);
            $cid = (int)($r['course_id'] ?? 0); if ($cid > 0) return $cid;
        }
        $p = finance_get_active_plan($db, $studentId);
        $cid = (int)($p['course_id'] ?? 0); if ($cid > 0) return $cid;
        $s = finance_get_student($db, $studentId);
        return (int)($s['course_id'] ?? 0);
    }
}

if (!function_exists('finance_record_payment')) {
    function finance_record_payment(mysqli $db, array $data): int {
        if (!fe_table_exists($db, 'payments')) throw new RuntimeException('payments table not found');
        $studentId = (int)($data['student_id'] ?? 0);
        if ($studentId <= 0) throw new InvalidArgumentException('Invalid student_id');

        $courseId = finance_resolve_course_id($db, $studentId, (int)($data['course_id'] ?? 0), (int)($data['plan_id'] ?? 0));
        $plan = finance_get_active_plan($db, $studentId, $courseId, (int)($data['plan_id'] ?? 0));
        $planId = (int)($plan['id'] ?? 0);
        $isInst = $plan && in_array(finance_plan_type($plan), ['installment','inst'], true);
        $scheduleId = (int)($data['schedule_id'] ?? 0);
        if ($isInst && $scheduleId <= 0) $scheduleId = (int)(finance_next_unpaid_schedule_id($db, $studentId, $planId, $courseId) ?? 0);

        $map = [
            'student_id' => $studentId,
            'amount' => fe_num($data['amount'] ?? 0),
            'course_id' => $courseId,
            'plan_id' => $planId,
            'schedule_id' => $scheduleId,
            'payment_method' => (string)($data['payment_method'] ?? $data['method'] ?? 'Cash'),
            'method' => (string)($data['payment_method'] ?? $data['method'] ?? 'Cash'),
            'reference_no' => (string)($data['reference_no'] ?? $data['ref_no'] ?? ''),
            'ref_no' => (string)($data['reference_no'] ?? $data['ref_no'] ?? ''),
            'notes' => (string)($data['notes'] ?? ''),
            'usd_amount' => fe_num($data['usd_amount'] ?? 0),
            'currency' => (string)($data['currency'] ?? 'LKR'),
            'fee_type' => (string)($data['fee_type'] ?? 'Mixed'),
            'items_json' => (string)($data['items_json'] ?? ''),
            'branch_name' => (string)($data['branch_name'] ?? ''),
            'cashier_name' => (string)($data['cashier_name'] ?? ''),
        ];

        if ($map['amount'] <= 0 && $map['usd_amount'] <= 0) throw new InvalidArgumentException('Amount cannot be 0');

        $cols = []; $vals = []; $types = ''; $binds = [];
        foreach ($map as $col => $val) {
            if (!fe_col_exists($db, 'payments', $col)) continue;
            if (in_array($col, ['course_id','plan_id','schedule_id'], true) && (int)$val <= 0) continue;
            $cols[] = $col; $vals[] = '?';
            if (is_int($val)) { $types .= 'i'; $binds[] = $val; }
            elseif (is_float($val)) { $types .= 'd'; $binds[] = $val; }
            else { $types .= 's'; $binds[] = (string)$val; }
        }
        $dateCol = fe_first_col($db, 'payments', ['date_paid','created_at']);
        if ($dateCol && !in_array($dateCol, $cols, true)) { $cols[] = $dateCol; $vals[] = 'NOW()'; }
        if (!$cols) throw new RuntimeException('No compatible payment columns found');

        $sql = "INSERT INTO payments (`" . implode('`,`', $cols) . "`) VALUES (" . implode(',', $vals) . ")";
        $st = $db->prepare($sql);
        if (!$st) throw new RuntimeException('Prepare failed: ' . $db->error);
        if ($types !== '') $st->bind_param($types, ...$binds);
        if (!$st->execute()) { $err = $st->error; $st->close(); throw new RuntimeException('Payment save failed: ' . $err); }
        $id = (int)$db->insert_id;
        $st->close();
        return $id;
    }
}
