<?php
/**
 * Expenses — Draft → Submitted → Reviewed → Approved → Paid.
 *
 * On approve() we post an accrual JE (Dr Expense / Cr AP).
 * On pay()     we post a settlement JE (Dr AP / Cr Bank/Cash per payment method).
 *
 * Both JEs carry Pacific analytical tags copied straight from the expense
 * record, so reports can slice by truck, driver, trip, customer, vendor,
 * cost centre without joining back to `expenses`.
 */
class Expenses extends Model
{
    private function s($v): string { return trim((string)$v); }
    private function m($v): float  { return (float)str_replace(',', '', (string)$v); }
    private function nn(int $v): ?int { return $v > 0 ? $v : null; }

    /**
     * payment_method → chart_of_accounts.account_code (Cr side on settlement JE).
     * Rewired to the client's COA on 2026-08-27 (Turn 1.5 · slice 21).
     * Cash + petty_cash lump into 3003. Bank routing defaults to CRDB; NMB is
     * available via the bank_accounts admin UI when Ops needs the second bank.
     */
    private const PAYMENT_METHOD_TO_CREDIT = [
        'cash'          => '3003',   // Cash
        'petty_cash'    => '3003',   // Cash (lumped — no separate petty cash line in client COA)
        'mobile_money'  => '3006',   // Mobile Money Float
        'bank_transfer' => '3001',   // Bank - CRDB (default; NMB=3002 configurable via bank_accounts)
        'cheque'        => '3001',   // Bank - CRDB
        'fuel_card'     => '3009',   // Fuel Card Account
    ];

    private const ACCOUNTS_PAYABLE_CODE = '4009';   // Trade Payables (was 2010)

    /* =========================================================
     *  Read
     * ========================================================= */
    public function listPaginated(int $page, int $perPage, array $filters = []): array
    {
        $where  = ['e.deleted = 0', 'e.tenant_id = :tenant'];
        $params = [':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1];

        if (!empty($filters['q'])) {
            $where[] = '(e.expense_no LIKE :q OR e.description LIKE :q OR et.display_name LIKE :q OR t.reg_no LIKE :q OR d.full_name LIKE :q OR v.vendor_name LIKE :q)';
            $params[':q'] = '%' . $filters['q'] . '%';
        }
        if (!empty($filters['status']))          { $where[] = 'e.status = :st';           $params[':st']  = $this->s($filters['status']); }
        if (!empty($filters['expense_type_id'])) { $where[] = 'e.expense_type_id = :etid'; $params[':etid']= (int)$filters['expense_type_id']; }
        if (!empty($filters['truck_id']))        { $where[] = 'e.truck_id = :tid';        $params[':tid'] = (int)$filters['truck_id']; }
        if (!empty($filters['driver_id']))       { $where[] = 'e.driver_id = :did';       $params[':did'] = (int)$filters['driver_id']; }
        if (!empty($filters['from_date']))       { $where[] = 'e.expense_date >= :fd';    $params[':fd']  = $this->s($filters['from_date']); }
        if (!empty($filters['to_date']))         { $where[] = 'e.expense_date <= :td';    $params[':td']  = $this->s($filters['to_date']); }

        $whereSql = implode(' AND ', $where);
        $db = new DB();

        $countRs = $db->mySelect(
            "SELECT COUNT(*) AS n
               FROM expenses e
          LEFT JOIN expense_types et ON et.c0d = e.expense_type_id
          LEFT JOIN trucks       t   ON t.c0d  = e.truck_id
          LEFT JOIN drivers      d   ON d.c0d  = e.driver_id
          LEFT JOIN vendors      v   ON v.c0d  = e.vendor_id
              WHERE $whereSql", $params
        );
        $total = (int)($countRs[0]['n'] ?? 0);

        $page    = max(1, $page);
        $perPage = max(1, min(200, $perPage));
        $offset  = ($page - 1) * $perPage;

        $rows = $db->mySelect(
            "SELECT e.c0d, e.expense_no, e.expense_date, e.description,
                    e.gross_amount, e.currency, e.status, e.payment_method,
                    e.truck_id, e.driver_id, e.trip_id, e.customer_id, e.vendor_id,
                    et.display_name AS type_name, et.scope,
                    t.reg_no        AS truck_reg,
                    d.full_name     AS driver_name,
                    v.vendor_name          AS vendor_name,
                    trp.trip_no     AS trip_no,
                    coa.account_code AS gl_code, coa.account_name AS gl_name
               FROM expenses e
          LEFT JOIN expense_types    et  ON et.c0d  = e.expense_type_id
          LEFT JOIN trucks           t   ON t.c0d   = e.truck_id
          LEFT JOIN drivers          d   ON d.c0d   = e.driver_id
          LEFT JOIN vendors          v   ON v.c0d   = e.vendor_id
          LEFT JOIN trips            trp ON trp.c0d = e.trip_id
          LEFT JOIN chart_of_accounts coa ON coa.c0d = et.gl_debit_account_id
              WHERE $whereSql
           ORDER BY e.status IN ('submitted','reviewed') DESC,
                    e.expense_date DESC, e.c0d DESC
              LIMIT $perPage OFFSET $offset",
            $params
        ) ?: [];

        return ['rows' => $rows, 'total' => $total];
    }

    /** Counts per status for the tabbed index header. */
    public function statusCounts(): array
    {
        $db = new DB();
        $rows = $db->mySelect(
            "SELECT status, COUNT(*) AS n FROM expenses WHERE deleted = 0 AND tenant_id = :t GROUP BY status",
            [':t' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1]
        ) ?: [];
        $out = ['draft'=>0,'submitted'=>0,'reviewed'=>0,'approved'=>0,'paid'=>0,'rejected'=>0,'cancelled'=>0];
        foreach ($rows as $r) $out[$r['status']] = (int)$r['n'];
        return $out;
    }

    public function byId(int $id): array
    {
        $db = new DB();
        $rs = $db->mySelect(
            "SELECT e.*,
                    et.display_name AS type_name, et.scope, et.slug AS type_slug,
                    et.gl_debit_account_id, coa.account_code AS gl_code, coa.account_name AS gl_name,
                    t.reg_no        AS truck_reg,
                    d.full_name     AS driver_name,
                    v.vendor_name          AS vendor_name,
                    c.fname         AS customer_name,
                    trp.trip_no     AS trip_no,
                    treq.request_no AS trip_request_no,
                    je_a.je_no      AS accrual_je_no,
                    je_p.je_no      AS payment_je_no
               FROM expenses e
          LEFT JOIN expense_types    et   ON et.c0d   = e.expense_type_id
          LEFT JOIN chart_of_accounts coa ON coa.c0d  = et.gl_debit_account_id
          LEFT JOIN trucks           t    ON t.c0d    = e.truck_id
          LEFT JOIN drivers          d    ON d.c0d    = e.driver_id
          LEFT JOIN vendors          v    ON v.c0d    = e.vendor_id
          LEFT JOIN customers        c    ON c.c0d    = e.customer_id
          LEFT JOIN trips            trp  ON trp.c0d  = e.trip_id
          LEFT JOIN trip_requests    treq ON treq.c0d = e.trip_request_id
          LEFT JOIN journal_entries  je_a ON je_a.c0d = e.accrual_je_id
          LEFT JOIN journal_entries  je_p ON je_p.c0d = e.payment_je_id
              WHERE e.c0d = :id AND e.deleted = 0
              LIMIT 1",
            [':id' => $id]
        );
        return $rs[0] ?? [];
    }

    public function approvalHistory(int $expenseId): array
    {
        return (new DB())->mySelect(
            "SELECT * FROM expense_approvals WHERE expense_id = :id ORDER BY action_at ASC, c0d ASC",
            [':id' => $expenseId]
        ) ?: [];
    }

    public function nextExpenseNo(): string
    {
        $rs = (new DB())->mySelect(
            "SELECT expense_no FROM expenses WHERE expense_no LIKE 'EXP-%' ORDER BY c0d DESC LIMIT 1", []
        );
        $last = (!empty($rs) && preg_match('/(\d+)$/', $rs[0]['expense_no'], $m)) ? (int)$m[1] : 0;
        return 'EXP-' . str_pad((string)($last + 1), 5, '0', STR_PAD_LEFT);
    }

    /* =========================================================
     *  Create / Update
     * ========================================================= */
    public function create(array $d, int $uid): array
    {
        $typeId = (int)($d['expense_type_id'] ?? 0);
        if (!$typeId) return ['ok' => false, 'msg' => 'Expense type is required.', 'id' => 0];

        $gross = $this->m($d['gross_amount'] ?? 0);
        $vat   = $this->m($d['vat_amount']   ?? 0);
        $net   = $gross - $vat;
        if ($gross <= 0) return ['ok' => false, 'msg' => 'Amount must be greater than zero.', 'id' => 0];

        $date = $this->s($d['expense_date'] ?? '') ?: date('Y-m-d');

        $db = new DB();
        $r  = $db->insertUpdate(
            "INSERT INTO expenses
                (tenant_id, expense_no, expense_type_id, expense_date, description,
                 net_amount, vat_amount, gross_amount, currency, fx_rate,
                 truck_id, driver_id, trip_id, trip_request_id, customer_id, vendor_id, cost_centre_id,
                 payment_method, payment_reference,
                 receipt_url, receipt_file_name, receipt_file_size, notes,
                 status, created_by, created_on)
             VALUES
                (:tenant, :en, :etid, :ed, :desc,
                 :net, :vat, :gross, :cur, :fx,
                 :tid, :did, :trip, :treq, :cid, :vid, :ccid,
                 :pm, :pref,
                 :rurl, :rname, :rsize, :notes,
                 :status, :uid, NOW())",
            [
                ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                ':en'     => $this->nextExpenseNo(),
                ':etid'   => $typeId,
                ':ed'     => $date,
                ':desc'   => $this->s($d['description'] ?? ''),
                ':net'    => round($net, 2),
                ':vat'    => round($vat, 2),
                ':gross'  => round($gross, 2),
                ':cur'    => $this->s(($d['currency'] ?? '') ?: (defined('DEFAULT_CURRENCY') ? DEFAULT_CURRENCY : 'TZS')),
                ':fx'     => (float)($d['fx_rate'] ?? 1.0) ?: 1.0,
                ':tid'    => $this->nn((int)($d['truck_id']        ?? 0)),
                ':did'    => $this->nn((int)($d['driver_id']       ?? 0)),
                ':trip'   => $this->nn((int)($d['trip_id']         ?? 0)),
                ':treq'   => $this->nn((int)($d['trip_request_id'] ?? 0)),
                ':cid'    => $this->nn((int)($d['customer_id']     ?? 0)),
                ':vid'    => $this->nn((int)($d['vendor_id']       ?? 0)),
                ':ccid'   => $this->nn((int)($d['cost_centre_id']  ?? 0)),
                ':pm'     => $this->s($d['payment_method']    ?? '') ?: null,
                ':pref'   => $this->s($d['payment_reference'] ?? ''),
                ':rurl'   => $this->s($d['receipt_url']       ?? ''),
                ':rname'  => $this->s($d['receipt_file_name'] ?? ''),
                ':rsize'  => isset($d['receipt_file_size']) ? (int)$d['receipt_file_size'] : null,
                ':notes'  => $this->s($d['notes'] ?? ''),
                ':status' => $this->s(($d['status'] ?? '') ?: 'draft'),
                ':uid'    => $uid,
            ]
        );

        $id = (int)$r['last_id'];
        if ($id > 0) $this->logTransition($id, null, $r['success'] > 0 ? ($d['status'] ?? 'draft') : 'draft', $uid, '', 'Expense created');
        return ['ok' => $r['success'] > 0, 'msg' => $r['success'] > 0 ? 'Expense saved.' : 'Save failed.', 'id' => $id];
    }

    public function update(int $id, array $d, int $uid): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Expense not found.', 'id' => 0];
        if (in_array($e['status'], ['approved','paid','rejected','cancelled'], true)) {
            return ['ok' => false, 'msg' => 'Cannot edit — status is ' . $e['status'] . '.', 'id' => 0];
        }
        $gross = $this->m($d['gross_amount'] ?? 0);
        $vat   = $this->m($d['vat_amount']   ?? 0);
        $net   = $gross - $vat;
        if ($gross <= 0) return ['ok' => false, 'msg' => 'Amount must be greater than zero.', 'id' => 0];

        (new DB())->insertUpdate(
            "UPDATE expenses SET
                expense_type_id = :etid, expense_date = :ed, description = :desc,
                net_amount = :net, vat_amount = :vat, gross_amount = :gross,
                currency = :cur, fx_rate = :fx,
                truck_id = :tid, driver_id = :did, trip_id = :trip, trip_request_id = :treq,
                customer_id = :cid, vendor_id = :vid, cost_centre_id = :ccid,
                payment_method = :pm, payment_reference = :pref,
                receipt_url = COALESCE(NULLIF(:rurl, ''), receipt_url),
                receipt_file_name = COALESCE(NULLIF(:rname, ''), receipt_file_name),
                receipt_file_size = COALESCE(:rsize, receipt_file_size),
                notes = :notes,
                edited_by = :uid, edited_on = NOW()
              WHERE c0d = :id",
            [
                ':etid'  => (int)($d['expense_type_id'] ?? $e['expense_type_id']),
                ':ed'    => $this->s($d['expense_date'] ?? '') ?: $e['expense_date'],
                ':desc'  => $this->s($d['description'] ?? ''),
                ':net'   => round($net, 2), ':vat' => round($vat, 2), ':gross' => round($gross, 2),
                ':cur'   => $this->s(($d['currency'] ?? '') ?: $e['currency']),
                ':fx'    => (float)($d['fx_rate'] ?? $e['fx_rate']) ?: 1.0,
                ':tid'   => $this->nn((int)($d['truck_id']        ?? 0)),
                ':did'   => $this->nn((int)($d['driver_id']       ?? 0)),
                ':trip'  => $this->nn((int)($d['trip_id']         ?? 0)),
                ':treq'  => $this->nn((int)($d['trip_request_id'] ?? 0)),
                ':cid'   => $this->nn((int)($d['customer_id']     ?? 0)),
                ':vid'   => $this->nn((int)($d['vendor_id']       ?? 0)),
                ':ccid'  => $this->nn((int)($d['cost_centre_id']  ?? 0)),
                ':pm'    => $this->s($d['payment_method']    ?? '') ?: null,
                ':pref'  => $this->s($d['payment_reference'] ?? ''),
                ':rurl'  => $this->s($d['receipt_url']       ?? ''),
                ':rname' => $this->s($d['receipt_file_name'] ?? ''),
                ':rsize' => isset($d['receipt_file_size']) ? (int)$d['receipt_file_size'] : null,
                ':notes' => $this->s($d['notes'] ?? ''),
                ':uid'   => $uid,
                ':id'    => $id,
            ]
        );
        return ['ok' => true, 'msg' => 'Expense updated.', 'id' => $id];
    }

    /* =========================================================
     *  Lifecycle transitions
     * ========================================================= */

    public function submit(int $id, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if ($e['status'] !== 'draft') return ['ok' => false, 'msg' => 'Only draft expenses can be submitted.'];
        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'submitted', submitted_by = :uid, submitted_at = NOW(),
                                  edited_by = :uid, edited_on = NOW() WHERE c0d = :id",
            [':uid' => $uid, ':id' => $id]
        );
        $this->logTransition($id, 'draft', 'submitted', $uid, $uname, 'Submitted for review');
        return ['ok' => true, 'msg' => 'Submitted for review.'];
    }

    public function review(int $id, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if ($e['status'] !== 'submitted') return ['ok' => false, 'msg' => 'Only submitted expenses can be reviewed.'];
        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'reviewed', reviewed_by = :uid, reviewed_at = NOW(),
                                  edited_by = :uid, edited_on = NOW() WHERE c0d = :id",
            [':uid' => $uid, ':id' => $id]
        );
        $this->logTransition($id, 'submitted', 'reviewed', $uid, $uname, 'Reviewed');
        return ['ok' => true, 'msg' => 'Reviewed and ready for approval.'];
    }

    /**
     * Approve — posts the accrual JE (Dr Expense / Cr AP) and links it back
     * to the expense record. Fails cleanly if the expense type has no GL
     * account mapped.
     */
    public function approve(int $id, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if ($e['status'] !== 'reviewed') return ['ok' => false, 'msg' => 'Only reviewed expenses can be approved.'];
        if (empty($e['gl_debit_account_id'])) {
            return ['ok' => false, 'msg' => 'Expense type "' . $e['type_name'] . '" has no GL account mapped — set it up in expense_types before approving.'];
        }

        $tags = $this->analyticalTags($e);

        $jp  = new JournalPosting();
        $res = $jp->post([
            'entry_date'     => $e['expense_date'],
            'source_type'    => 'EXPENSE',
            'source_id'      => $id,
            'source_ref'     => $e['expense_no'],
            'narrative'      => $e['type_name'] . ' — ' . ($e['description'] ?: $e['expense_no']),
            'currency'       => $e['currency'],
            'fx_rate'        => (float)($e['fx_rate'] ?? 1.0),
            'posted_by'      => $uid,
            'posted_by_name' => $uname,
            'lines' => [
                array_merge([
                    'account_id' => (int)$e['gl_debit_account_id'],
                    'debit'      => (float)$e['gross_amount'],
                    'credit'     => 0,
                    'narrative'  => 'Expense accrual · ' . $e['expense_no'],
                ], $tags),
                array_merge([
                    'account_code' => self::ACCOUNTS_PAYABLE_CODE,
                    'debit'        => 0,
                    'credit'       => (float)$e['gross_amount'],
                    'narrative'    => 'Payable for ' . $e['expense_no'],
                ], $tags),
            ],
        ]);

        if (!$res['ok']) return ['ok' => false, 'msg' => 'Journal posting failed: ' . $res['msg']];

        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'approved', accrual_je_id = :jeid,
                                  approved_by = :uid, approved_at = NOW(),
                                  edited_by = :uid, edited_on = NOW()
              WHERE c0d = :id",
            [':jeid' => $res['id'], ':uid' => $uid, ':id' => $id]
        );
        $this->logTransition($id, 'reviewed', 'approved', $uid, $uname, 'Approved · JE ' . $res['je_no'] . ' posted');
        return ['ok' => true, 'msg' => 'Approved · journal ' . $res['je_no'] . ' posted.'];
    }

    public function reject(int $id, string $reason, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if (!in_array($e['status'], ['draft','submitted','reviewed'], true)) {
            return ['ok' => false, 'msg' => 'Cannot reject expenses that are approved / paid.'];
        }
        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'rejected', rejected_by = :uid, rejected_at = NOW(),
                                  rejection_reason = :rr,
                                  edited_by = :uid, edited_on = NOW()
              WHERE c0d = :id",
            [':uid' => $uid, ':rr' => $this->s($reason), ':id' => $id]
        );
        $this->logTransition($id, $e['status'], 'rejected', $uid, $uname, $this->s($reason) ?: 'Rejected');
        return ['ok' => true, 'msg' => 'Rejected.'];
    }

    public function cancel(int $id, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if (in_array($e['status'], ['approved','paid','cancelled','rejected'], true)) {
            return ['ok' => false, 'msg' => 'Cannot cancel — status is ' . $e['status'] . '.'];
        }
        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'cancelled', edited_by = :uid, edited_on = NOW() WHERE c0d = :id",
            [':uid' => $uid, ':id' => $id]
        );
        $this->logTransition($id, $e['status'], 'cancelled', $uid, $uname, 'Cancelled');
        return ['ok' => true, 'msg' => 'Cancelled.'];
    }

    /**
     * Pay — posts settlement JE (Dr AP / Cr Bank or Cash per payment_method).
     * Updates payment_method + payment_reference on the expense so the record
     * carries how it was actually settled.
     */
    public function pay(int $id, array $d, int $uid, string $uname): array
    {
        $e = $this->byId($id);
        if (!$e) return ['ok' => false, 'msg' => 'Not found.'];
        if ($e['status'] !== 'approved') return ['ok' => false, 'msg' => 'Only approved expenses can be paid.'];

        $method  = $this->s($d['payment_method']    ?? '') ?: ($e['payment_method'] ?: 'cash');
        $ref     = $this->s($d['payment_reference'] ?? '');
        $payDate = $this->s($d['payment_date']      ?? '') ?: date('Y-m-d');
        $creditCode = self::PAYMENT_METHOD_TO_CREDIT[$method] ?? '1010';

        $tags = $this->analyticalTags($e);
        $jp   = new JournalPosting();
        $res  = $jp->post([
            'entry_date'     => $payDate,
            'source_type'    => 'PAYMENT',
            'source_id'      => $id,
            'source_ref'     => $e['expense_no'] . '/PAY',
            'narrative'      => 'Payment of ' . $e['expense_no'] . ($ref ? ' · ' . $ref : ''),
            'currency'       => $e['currency'],
            'fx_rate'        => (float)($e['fx_rate'] ?? 1.0),
            'posted_by'      => $uid,
            'posted_by_name' => $uname,
            'lines' => [
                array_merge([
                    'account_code' => self::ACCOUNTS_PAYABLE_CODE,
                    'debit'        => (float)$e['gross_amount'],
                    'credit'       => 0,
                    'narrative'    => 'Settle payable · ' . $e['expense_no'],
                ], $tags),
                array_merge([
                    'account_code' => $creditCode,
                    'debit'        => 0,
                    'credit'       => (float)$e['gross_amount'],
                    'narrative'    => 'Paid via ' . $method . ($ref ? ' · ' . $ref : ''),
                ], $tags),
            ],
        ]);
        if (!$res['ok']) return ['ok' => false, 'msg' => 'Payment JE failed: ' . $res['msg']];

        (new DB())->insertUpdate(
            "UPDATE expenses SET status = 'paid',
                                  payment_method    = :pm,
                                  payment_reference = COALESCE(NULLIF(:pref,''), payment_reference),
                                  payment_je_id     = :jeid,
                                  paid_by = :uid, paid_at = NOW(),
                                  edited_by = :uid, edited_on = NOW()
              WHERE c0d = :id",
            [':pm' => $method, ':pref' => $ref, ':jeid' => $res['id'], ':uid' => $uid, ':id' => $id]
        );
        $this->logTransition($id, 'approved', 'paid', $uid, $uname, 'Paid via ' . $method . ' · JE ' . $res['je_no']);
        return ['ok' => true, 'msg' => 'Paid · journal ' . $res['je_no'] . ' posted.'];
    }

    public function softDelete(int $id, int $uid): array
    {
        (new DB())->insertUpdate(
            "UPDATE expenses SET deleted = 1, edited_by = :uid, edited_on = NOW() WHERE c0d = :id",
            [':uid' => $uid, ':id' => $id]
        );
        return ['ok' => true];
    }

    /* =========================================================
     *  Helpers
     * ========================================================= */
    private function analyticalTags(array $e): array
    {
        return [
            'truck_id'        => (int)($e['truck_id']       ?? 0) ?: 0,
            'driver_id'       => (int)($e['driver_id']      ?? 0) ?: 0,
            'trip_id'         => (int)($e['trip_id']        ?? 0) ?: 0,
            'trip_request_id' => (int)($e['trip_request_id']?? 0) ?: 0,
            'customer_id'     => (int)($e['customer_id']    ?? 0) ?: 0,
            'vendor_id'       => (int)($e['vendor_id']      ?? 0) ?: 0,
            'expense_id'      => (int)($e['c0d']            ?? 0) ?: 0,
            'cost_centre_id'  => (int)($e['cost_centre_id'] ?? 0) ?: 0,
        ];
    }

    private function logTransition(int $expenseId, ?string $from, string $to, int $uid, string $uname, string $note = ''): void
    {
        (new DB())->insertUpdate(
            "INSERT INTO expense_approvals (expense_id, from_status, to_status, actor_id, actor_name, action_at, note)
             VALUES (:eid, :from, :to, :uid, :name, NOW(), :note)",
            [':eid' => $expenseId, ':from' => $from, ':to' => $to, ':uid' => $uid, ':name' => $uname, ':note' => $note]
        );
    }
}
