<?php

declare(strict_types=1);

require DENTAL_ROOT . '/app/bootstrap.php';
require_login();
require_permission('billing.manage', ['super_admin','clinic_admin','cashier','staff']);
require DENTAL_ROOT . '/app/layout.php';
require_once DENTAL_ROOT . '/app/module_helpers.php';
require_once DENTAL_ROOT . '/app/transaction_integrity.php';
require_once DENTAL_ROOT . '/app/revenue_integrity.php';
if (is_file(DENTAL_ROOT . '/app/role_satisfaction.php')) { require_once DENTAL_ROOT . '/app/role_satisfaction.php'; }
require_once DENTAL_ROOT . '/app/inventory_integrity.php';
require_once DENTAL_ROOT . '/app/appointment_planning.php';
require_once DENTAL_ROOT . '/app/clinical_billing.php';
require_once DENTAL_ROOT . '/app/workflow_governance.php';


if (!function_exists('dental_cashier_my_report')) {
    function dental_cashier_my_report(PDO $pdo, int $branchId, int $cashierId): array
    {
        return [
            'payments_today' => 0,
            'amount_today' => 0.0,
            'pending_returns' => 0,
            'session_rows' => [],
        ];
    }
}

$pdo = db();
$userId = (int)(current_user()['id'] ?? 0);
$branchId = signed_user_branch_id();
if ($branchId < 1 && !superuser_primary_workspace()) {
    http_response_code(403);
    exit('Assign this finance user to a clinic branch before opening Billing.');
}
if ($branchId < 1 && superuser_primary_workspace()) {
    $branchId = (int)($pdo->query('SELECT id FROM `' . table('branches') . '` WHERE is_active=1 ORDER BY id LIMIT 1')->fetchColumn() ?: 0);
}
$isSupervisor = in_array(actual_role(), ['super_admin','clinic_admin'], true) && active_role() === actual_role();
$isCashierOperator = in_array(active_role(), ['cashier','staff'], true) && active_role_can_operate(active_role());
$financeView = trim((string)($_POST['return_view'] ?? $_GET['view'] ?? 'register'));
if (!in_array($financeView, ['register','exceptions'], true)) $financeView = 'register';
if ($financeView === 'exceptions' && !$isSupervisor) $financeView = 'register';
$error = '';

function cashier_finance_return_url(string $default = 'billing.php'): string
{
    return ((string)($_POST['return_view'] ?? '') === 'exceptions')
        ? 'billing.php?view=exceptions'
        : $default;
}

$posReady = schema_table_exists(table('invoice_items'))
    && schema_column_exists(table('service_catalog'), 'standard_fee')
    && schema_column_exists(table('invoices'), 'subtotal')
    && schema_column_exists(table('payments'), 'tendered_amount')
    && revenue_schema_ready();
$cashierSchemaIssues = cashier_register_schema_issues();
$posReady = $posReady && !$cashierSchemaIssues;

// Resolve the patient-search endpoint before any page output. The previous
// register template called a stale app_url() helper while rendering, which
// caused the request error card to be appended inside an otherwise loaded POS.
// app_absolute_url() is the canonical helper; the relative fallback keeps the
// register usable even when the configured application URL is temporarily bad.
// Keep the endpoint relative to the current DentalCare origin. A configured
// canonical URL may point to another host or scheme, which would make the
// browser omit the signed-in session and cause the patient picker to fail.
$cashierPatientSearchEndpoint = 'api/patient_search.php';


/**
 * Return true only when every requested column exists. Billing is opened by
 * users during active checkout, so a partially applied historical migration
 * must degrade to a clear compatibility warning instead of a fatal page error.
 */
function cashier_schema_has_columns(string $tableName, array $columns): bool
{
    if (!schema_table_exists($tableName)) return false;
    foreach ($columns as $column) {
        if (!schema_column_exists($tableName, (string)$column)) return false;
    }
    return true;
}

/**
 * Report structural gaps that affect the procedure-first register. The page
 * can still show the queue and legacy balances while the Superuser applies the
 * database repair.
 */
function cashier_register_schema_issues(): array
{
    $requirements = [
        table('queue_entries') => ['patient_id','branch_id','status','checked_in_at','billing_at'],
        table('clinical_encounters') => ['patient_id','branch_id','queue_entry_id','status','created_at','finalized_at'],
        table('procedures') => ['patient_id','branch_id','service_id','encounter_id','treatment_plan_item_id','procedure_name','service_name_snapshot','approved_fee_snapshot','fee_status','no_charge_reason','fee_authorized_by','fee_authorized_at','billing_status','status'],
        table('treatment_plan_items') => ['id','estimated_fee'],
        table('invoices') => ['patient_id','branch_id','encounter_id','queue_entry_id','status','amount','paid_amount'],
        table('invoice_items') => ['invoice_id','procedure_id','clinical_usage_item_id','description','quantity','unit_price','gross_amount'],
        table('clinical_usage_items') => ['id','encounter_id','description','quantity','unit_price','line_total','usage_type','billing_mode','fulfillment_status'],
        table('payments') => ['invoice_id','amount','method','created_at'],
    ];
    $issues = [];
    foreach ($requirements as $tableName => $columns) {
        if (!schema_table_exists($tableName)) {
            $issues[] = $tableName . ' table is missing';
            continue;
        }
        foreach ($columns as $column) {
            if (!schema_column_exists($tableName, $column)) $issues[] = $tableName . '.' . $column;
        }
    }
    return $issues;
}

/**
 * Older upgraded databases may not yet contain procedures.service_id or one
 * of the treatment-fee columns. Build only the joins that the live schema can
 * support, then show a zero approved amount until the repair is applied.
 */
function cashier_procedure_charge_fragments(string $alias): array
{
    static $cache = [];
    if (isset($cache[$alias])) return $cache[$alias];

    $procedureTable = table('procedures');
    $planTable = table('treatment_plan_items');
    $serviceTable = table('service_catalog');
    $hasProcedureService = schema_column_exists($procedureTable, 'service_id');
    $hasPlanLink = schema_column_exists($procedureTable, 'treatment_plan_item_id')
        && cashier_schema_has_columns($planTable, ['id','estimated_fee']);
    $hasServiceFee = $hasProcedureService
        && cashier_schema_has_columns($serviceTable, ['id','standard_fee']);
    $hasSnapshot = cashier_schema_has_columns($procedureTable, ['service_name_snapshot','approved_fee_snapshot','fee_status','no_charge_reason','fee_authorized_by','fee_authorized_at','billing_status']);

    if ($hasSnapshot) {
        $feeExpression = 'COALESCE(' . $alias . '.approved_fee_snapshot,0)';
        $nameExpression = 'COALESCE(NULLIF(' . $alias . '.service_name_snapshot,""),' . $alias . '.procedure_name)';
        $feeStatusSelect = $alias . '.fee_status AS fee_status';
        $noChargeSelect = $alias . '.no_charge_reason AS no_charge_reason';
        $authorizedBySelect = $alias . '.fee_authorized_by AS fee_authorized_by';
        $authorizedAtSelect = $alias . '.fee_authorized_at AS fee_authorized_at';
        $billingStatusSelect = $alias . '.billing_status AS billing_status';
    } else {
        $feeParts = [];
        if ($hasPlanLink) $feeParts[] = 'NULLIF(tpi.estimated_fee,0)';
        if ($hasServiceFee) $feeParts[] = 'NULLIF(sc.standard_fee,0)';
        $feeParts[] = '0';
        $feeExpression = count($feeParts) > 1 ? 'COALESCE(' . implode(',', $feeParts) . ')' : '0';
        $nameExpression = $alias . '.procedure_name';
        $feeStatusSelect = 'CASE WHEN ' . $feeExpression . '>0 THEN "billable" ELSE "blocked" END AS fee_status';
        $noChargeSelect = 'NULL AS no_charge_reason';
        $authorizedBySelect = 'NULL AS fee_authorized_by';
        $authorizedAtSelect = 'NULL AS fee_authorized_at';
        $billingStatusSelect = 'CASE WHEN ' . $feeExpression . '>0 THEN "ready" ELSE "blocked" END AS billing_status';
    }

    return $cache[$alias] = [
        'service_select' => $hasProcedureService ? $alias . '.service_id AS service_id' : 'NULL AS service_id',
        'plan_join' => $hasPlanLink ? ' LEFT JOIN `' . $planTable . '` tpi ON tpi.id=' . $alias . '.treatment_plan_item_id ' : ' ',
        'service_join' => $hasServiceFee ? ' LEFT JOIN `' . $serviceTable . '` sc ON sc.id=' . $alias . '.service_id ' : ' ',
        'name_expression' => $nameExpression,
        'fee_expression' => $feeExpression,
        'fee_status_select' => $feeStatusSelect,
        'no_charge_select' => $noChargeSelect,
        'authorized_by_select' => $authorizedBySelect,
        'authorized_at_select' => $authorizedAtSelect,
        'billing_status_select' => $billingStatusSelect,
        'has_snapshot' => $hasSnapshot,
    ];
}


/**
 * Return the Dentist-finalized procedures and separately billable clinical
 * handoffs for one encounter. This powers both the cashier queue preview and
 * the register, so the cashier always sees what was actually done and the
 * approved amount before taking payment.
 */
function cashier_encounter_charge_preview(PDO $pdo, int $encounterId): array
{
    $result = [
        'items'=>[],
        'total'=>0.0,
        'procedure_total'=>0.0,
        'clinical_usage_total'=>0.0,
        'unbilled_total'=>0.0,
        'missing_fee_count'=>0,
        'unbilled_count'=>0,
        'summary'=>'No finalized procedure recorded',
        'compatibility_error'=>null,
    ];
    if ($encounterId < 1) return $result;

    $procedureTable = table('procedures');
    if (!cashier_schema_has_columns($procedureTable, ['id','encounter_id','procedure_name','status'])) {
        $result['compatibility_error'] = 'The procedure register requires a database repair.';
        return $result;
    }

    try {
        $procedureCharge = cashier_procedure_charge_fragments('pr');
        $invoiceItemTable = table('invoice_items');
        $invoiceTable = table('invoices');
        $canCheckProcedureBilling = cashier_schema_has_columns($invoiceItemTable, ['invoice_id','procedure_id'])
            && cashier_schema_has_columns($invoiceTable, ['id','status']);
        $billedSelect = $canCheckProcedureBilling
            ? ',EXISTS(SELECT 1 FROM `' . $invoiceItemTable . '` ii JOIN `' . $invoiceTable . '` inv ON inv.id=ii.invoice_id WHERE ii.procedure_id=pr.id AND inv.status NOT IN ("voided","refunded")) billed'
            : ',0 billed';
        $toothSelect = schema_column_exists($procedureTable, 'tooth_region') ? 'pr.tooth_region' : 'NULL tooth_region';
        $stmt = $pdo->prepare(
            'SELECT pr.id,' . $procedureCharge['name_expression'] . ' description,' . $toothSelect . ',1 quantity,'
            . $procedureCharge['fee_expression'] . ' unit_price,' . $procedureCharge['fee_status_select'] . ','
            . $procedureCharge['no_charge_select'] . ',' . $procedureCharge['authorized_by_select'] . ','
            . $procedureCharge['authorized_at_select'] . ',' . $procedureCharge['billing_status_select'] . $billedSelect . ' '
            . 'FROM `' . $procedureTable . '` pr '
            . $procedureCharge['plan_join']
            . $procedureCharge['service_join']
            . 'WHERE pr.encounter_id=? AND pr.status="completed" ORDER BY pr.id'
        );
        $stmt->execute([$encounterId]);
        foreach ($stmt->fetchAll() as $row) {
            $price = round((float)($row['unit_price'] ?? 0), 2);
            $description = trim((string)$row['description'] . (!empty($row['tooth_region']) ? ' · ' . (string)$row['tooth_region'] : ''));
            $item = [
                'source'=>'procedure',
                'description'=>$description,
                'quantity'=>1.0,
                'unit_price'=>$price,
                'line_total'=>$price,
                'fee_status'=>(string)($row['fee_status'] ?? 'blocked'),
                'no_charge_reason'=>(string)($row['no_charge_reason'] ?? ''),
                'billing_status'=>(string)($row['billing_status'] ?? 'blocked'),
                'billed'=>(bool)($row['billed'] ?? false),
            ];
            $result['items'][] = $item;
            $result['total'] += $price;
            $result['procedure_total'] += $price;
            $missingApprovedFee = $price <= 0.001 && (string)($row['fee_status'] ?? 'blocked') !== 'no_charge';
            if ($missingApprovedFee) $result['missing_fee_count']++;
            if (!$item['billed']) {
                $result['unbilled_count']++;
                $result['unbilled_total'] += $price;
            }
        }

        $usageTable = table('clinical_usage_items');
        $usageRequired = ['id','encounter_id','description','quantity','unit_price','line_total','usage_type','billing_mode','fulfillment_status'];
        if (cashier_schema_has_columns($usageTable, $usageRequired)) {
            $canCheckUsageBilling = cashier_schema_has_columns($invoiceItemTable, ['invoice_id','clinical_usage_item_id'])
                && cashier_schema_has_columns($invoiceTable, ['id','status']);
            $usageBilled = $canCheckUsageBilling
                ? ',EXISTS(SELECT 1 FROM `' . $invoiceItemTable . '` ii JOIN `' . $invoiceTable . '` inv ON inv.id=ii.invoice_id WHERE ii.clinical_usage_item_id=cu.id AND inv.status NOT IN ("voided","refunded")) billed'
                : ',0 billed';
            $unitSelect = schema_column_exists($usageTable, 'unit') ? 'cu.unit' : 'NULL unit';
            $usage = $pdo->prepare(
                'SELECT cu.id,cu.description,cu.quantity,' . $unitSelect . ',cu.unit_price,cu.line_total,cu.usage_type' . $usageBilled . ' '
                . 'FROM `' . $usageTable . '` cu '
                . 'WHERE cu.encounter_id=? AND cu.billing_mode="bill_separately" AND cu.fulfillment_status IN ("requested","confirmed") ORDER BY cu.id'
            );
            $usage->execute([$encounterId]);
            foreach ($usage->fetchAll() as $row) {
                $line = round((float)($row['line_total'] ?? ((float)$row['quantity'] * (float)$row['unit_price'])), 2);
                $item = [
                    'source'=>(string)$row['usage_type'],
                    'description'=>(string)$row['description'],
                    'quantity'=>(float)$row['quantity'],
                    'unit'=>(string)($row['unit'] ?? ''),
                    'unit_price'=>(float)$row['unit_price'],
                    'line_total'=>$line,
                    'billed'=>(bool)($row['billed'] ?? false),
                ];
                $result['items'][] = $item;
                $result['total'] += $line;
                $result['clinical_usage_total'] += $line;
                if (!$item['billed']) {
                    $result['unbilled_count']++;
                    $result['unbilled_total'] += $line;
                    if ($line <= 0.001) $result['missing_fee_count']++;
                }
            }
        }
    } catch (Throwable $previewError) {
        error_log('Cashier encounter preview compatibility fallback for encounter #' . $encounterId . ': ' . $previewError->getMessage());
        $result['compatibility_error'] = 'Some clinical charge fields require the pending database repair.';
    }

    $result['total'] = round((float)$result['total'], 2);
    $result['procedure_total'] = round((float)$result['procedure_total'], 2);
    $result['clinical_usage_total'] = round((float)$result['clinical_usage_total'], 2);
    $result['unbilled_total'] = round((float)$result['unbilled_total'], 2);
    if ($result['items']) {
        $names = array_map(static fn(array $item): string => (string)$item['description'], array_slice($result['items'], 0, 2));
        $result['summary'] = implode(' · ', $names);
        if (count($result['items']) > 2) $result['summary'] .= ' +' . (count($result['items']) - 2) . ' more';
    }
    return $result;
}

function cashier_direct_encounter_candidates(PDO $pdo, int $patientId, int $branchId): array
{
    $encounterTable = table('clinical_encounters');
    if ($patientId < 1 || !cashier_schema_has_columns($encounterTable, ['id','patient_id','status','created_at'])) return [];

    $branchFilter = schema_column_exists($encounterTable, 'branch_id')
        ? ' AND (ce.branch_id=? OR ce.branch_id IS NULL) '
        : ' ';
    $params = [$patientId];
    if ($branchFilter !== ' ') $params[] = $branchId;
    $finalizedSelect = schema_column_exists($encounterTable, 'finalized_at') ? 'ce.finalized_at' : 'NULL finalized_at';
    $dentistSelect = schema_column_exists($encounterTable, 'dentist_name') ? 'ce.dentist_name' : 'NULL dentist_name';
    $orderParts = [];
    if (schema_column_exists($encounterTable, 'finalized_at')) $orderParts[] = 'ce.finalized_at';
    if (schema_column_exists($encounterTable, 'updated_at')) $orderParts[] = 'ce.updated_at';
    $orderParts[] = 'ce.created_at';

    try {
        $stmt = $pdo->prepare(
            'SELECT ce.id,ce.created_at,' . $finalizedSelect . ',' . $dentistSelect . ',ce.status '
            . 'FROM `' . $encounterTable . '` ce '
            . 'WHERE ce.patient_id=?' . $branchFilter . 'AND ce.status IN ("finalized","addendum") '
            . 'ORDER BY ' . (count($orderParts) > 1 ? 'COALESCE(' . implode(',', $orderParts) . ')' : $orderParts[0]) . ' DESC,ce.id DESC LIMIT 20'
        );
        $stmt->execute($params);
        $rows = [];
        foreach ($stmt->fetchAll() as $row) {
            $preview = cashier_encounter_charge_preview($pdo,(int)$row['id']);
            if ((int)$preview['unbilled_count'] < 1 && empty($preview['compatibility_error'])) continue;
            $row['charge_preview'] = $preview;
            $rows[] = $row;
        }
        return $rows;
    } catch (Throwable $encounterError) {
        error_log('Cashier direct encounter lookup compatibility fallback: ' . $encounterError->getMessage());
        return [];
    }
}

function cashier_queue_history(PDO $pdo, int $queueId, string $oldStatus, string $newStatus, ?string $note = null): void
{
    $stmt = $pdo->prepare(
        'INSERT INTO `' . table('queue_status_history') . '` '
        . '(queue_entry_id,previous_status,new_status,notes,changed_by,created_at) VALUES (?,?,?,?,?,NOW())'
    );
    $stmt->execute([$queueId, $oldStatus, $newStatus, $note, current_user()['id'] ?? null]);
}

function cashier_pending_followup_for_queue(PDO $pdo, int $queueId, int $patientId): ?array
{
    if (!schema_table_exists(table('follow_ups')) || !schema_table_exists(table('clinical_encounters'))) return null;
    $stmt = $pdo->prepare(
        'SELECT f.id,f.recommended_date,f.preferred_time,f.urgency FROM `' . table('follow_ups') . '` f '
        . 'JOIN `' . table('clinical_encounters') . '` ce ON ce.id=f.encounter_id '
        . 'WHERE ce.queue_entry_id=? AND f.patient_id=? AND f.appointment_id IS NULL '
        . 'AND f.status="pending" ORDER BY FIELD(f.urgency,"urgent","important","routine"),f.recommended_date,f.id LIMIT 1'
    );
    $stmt->execute([$queueId,$patientId]);
    return $stmt->fetch() ?: null;
}

function cashier_complete_visit(PDO $pdo, int $queueId, int $patientId, ?int $appointmentId, string $completionDetail = 'All queue-linked balances cleared.'): void
{
    $queueStmt = $pdo->prepare('SELECT status FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? FOR UPDATE');
    $queueStmt->execute([$queueId, $patientId]);
    $oldStatus = $queueStmt->fetchColumn();
    if ($oldStatus === false || $oldStatus === 'completed') return;
    if ((string)$oldStatus !== 'cashier_called') {
        throw new RuntimeException('Call the patient at the cashier before completing the visit.');
    }
    $pendingFollowup = cashier_pending_followup_for_queue($pdo,$queueId,$patientId);
    if ($pendingFollowup) {
        throw new RuntimeException('Book the Dentist-recommended follow-up appointment, or document why it will be scheduled later, before completing cashier checkout.');
    }
    $queueUpdate = $pdo->prepare(
        'UPDATE `' . table('queue_entries') . '` SET status="completed",paid_at=COALESCE(paid_at,NOW()),completed_at=COALESCE(completed_at,NOW()),updated_at=NOW() WHERE id=? AND status="cashier_called"'
    );
    $queueUpdate->execute([$queueId]);
    if ($queueUpdate->rowCount() !== 1) {
        throw new RuntimeException('The cashier queue state changed while the visit was being completed.');
    }
    cashier_queue_history($pdo, $queueId, (string)$oldStatus, 'completed', $completionDetail);

    if ($appointmentId) {
        $appointment = $pdo->prepare('SELECT status,start_at FROM `' . table('appointments') . '` WHERE id=? FOR UPDATE');
        $appointment->execute([$appointmentId]);
        $appointmentState = $appointment->fetch();
        $oldAppointmentStatus = $appointmentState['status'] ?? false;
        if ($oldAppointmentStatus !== false && $oldAppointmentStatus !== 'completed') {
            integrity_assert_appointment_transition((string)$oldAppointmentStatus, 'completed', (string)($appointmentState['start_at'] ?? ''));
            $appointmentUpdate = $pdo->prepare('UPDATE `' . table('appointments') . '` SET status="completed",updated_at=NOW() WHERE id=? AND status=?');
            $appointmentUpdate->execute([$appointmentId, (string)$oldAppointmentStatus]);
            if ($appointmentUpdate->rowCount() !== 1) {
                throw new RuntimeException('The linked appointment changed while cashier completion was being recorded.');
            }
            $pdo->prepare(
                'INSERT INTO `' . table('appointment_status_history') . '` '
                . '(appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?, ?, "completed", "Completed at cashier", ?, NOW())'
            )->execute([$appointmentId, $oldAppointmentStatus, current_user()['id'] ?? null]);
        }
        $pdo->prepare('UPDATE `' . table('follow_ups') . '` SET status="completed",completion_notes=COALESCE(completion_notes,"Visit completed at cashier") WHERE appointment_id=? AND status="scheduled"')->execute([$appointmentId]);
        $pdo->prepare('UPDATE `' . table('recalls') . '` SET status="completed" WHERE appointment_id=? AND status="scheduled"')->execute([$appointmentId]);
    }
    workflow_event($patientId, 'visit_completed', 'Visit completed at cashier', ['appointment_id'=>$appointmentId,'queue_entry_id'=>$queueId], $completionDetail);
}

function cashier_send_payment_receipt(PDO $pdo, int $paymentId): array
{
    if (!schema_column_exists(table('payments'), 'receipt_email_outbox_id')) {
        return ['id'=>0,'status'=>'not_available','error'=>'Tracked payment receipt email is not available.','attempts'=>0];
    }
    $stmt = $pdo->prepare(
        'SELECT pay.id,pay.amount,pay.method,pay.reference_no,pay.created_at,i.id invoice_id,i.invoice_no,'
        . 'p.first_name,p.last_name,p.email FROM `' . table('payments') . '` pay '
        . 'JOIN `' . table('invoices') . '` i ON i.id=pay.invoice_id '
        . 'JOIN `' . table('patients') . '` p ON p.id=i.patient_id WHERE pay.id=? LIMIT 1'
    );
    $stmt->execute([$paymentId]);
    $payment = $stmt->fetch();
    if (!$payment) throw new RuntimeException('Payment record not found.');
    $email = trim((string)($payment['email'] ?? ''));
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['id'=>0,'status'=>'not_available','error'=>'The patient has no valid email address.','attempts'=>0];
    }
    $receiptNumber = (string)$payment['invoice_no'] . '-P' . str_pad((string)$paymentId, 4, '0', STR_PAD_LEFT);
    return queue_template_email('payment_receipt', $email, [
        'recipient_name'=>trim((string)$payment['first_name'] . ' ' . (string)$payment['last_name']),
        'amount'=>'PHP ' . number_format((float)$payment['amount'], 2),
        'receipt_number'=>$receiptNumber,
        'payment_date'=>date('F j, Y g:i A', strtotime((string)$payment['created_at'])),
    ], ['type'=>'payment_receipt','payment_id'=>$paymentId,'invoice_id'=>(int)$payment['invoice_id']], true);
}

function cashier_invoice_number(PDO $pdo): string
{
    for ($attempt = 0; $attempt < 10; $attempt++) {
        $number = 'INV-' . date('ymd-His') . '-' . random_int(10, 99);
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM `' . table('invoices') . '` WHERE invoice_no=?');
        $stmt->execute([$number]);
        if ((int)$stmt->fetchColumn() === 0) return $number;
    }
    throw new RuntimeException('Unable to generate a unique invoice number.');
}

function cashier_required_procedure_ids(PDO $pdo, int $encounterId, int $invoiceId): array
{
    if ($encounterId < 1) return [];
    $stmt = $pdo->prepare(
        'SELECT DISTINCT p.id FROM `' . table('procedures') . '` p '
        . 'WHERE p.encounter_id=? AND p.status="completed" AND ('
        . 'NOT EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` ii JOIN `' . table('invoices') . '` i ON i.id=ii.invoice_id '
        . 'WHERE ii.procedure_id=p.id AND i.id<>? AND i.status NOT IN ("voided","refunded")) '
        . 'OR EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` current_ii WHERE current_ii.invoice_id=? AND current_ii.procedure_id=p.id)'
        . ') ORDER BY p.id'
    );
    $stmt->execute([$encounterId, $invoiceId, $invoiceId]);
    return array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
}

function cashier_required_usage_ids(PDO $pdo, int $encounterId, int $invoiceId): array
{
    if ($encounterId < 1 || !schema_table_exists(table('clinical_usage_items'))) return [];
    $stmt=$pdo->prepare(
        'SELECT cu.id FROM `' . table('clinical_usage_items') . '` cu '
        . 'WHERE cu.encounter_id=? AND cu.billing_mode="bill_separately" AND cu.line_total>0 AND cu.fulfillment_status IN ("requested","confirmed") AND ('
        . 'NOT EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` ii JOIN `' . table('invoices') . '` i ON i.id=ii.invoice_id WHERE ii.clinical_usage_item_id=cu.id AND i.id<>? AND i.status NOT IN ("voided","refunded")) '
        . 'OR EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` current_ii WHERE current_ii.invoice_id=? AND current_ii.clinical_usage_item_id=cu.id)) ORDER BY cu.id'
    );
    $stmt->execute([$encounterId,$invoiceId,$invoiceId]);
    return array_map('intval',$stmt->fetchAll(PDO::FETCH_COLUMN));
}

function cashier_confirm_usage_items(PDO $pdo,array $items,int $branchId,int $userId): void
{
    if(!schema_table_exists(table('clinical_usage_items')))return;
    foreach($items as $item){
        $usageId=(int)($item['clinical_usage_item_id']??0);
        if($usageId<1)continue;
        $stmt=$pdo->prepare('SELECT * FROM `' . table('clinical_usage_items') . '` WHERE id=? AND branch_id=? FOR UPDATE');
        $stmt->execute([$usageId,$branchId]);
        $usage=$stmt->fetch();
        if(!$usage)throw new RuntimeException('A clinical medicine or material handoff is no longer available. Reload the cashier register.');
        if((string)$usage['fulfillment_status']==='confirmed')continue;
        if((string)$usage['fulfillment_status']!=='requested')throw new RuntimeException('A clinical medicine or material handoff is not ready for confirmation.');
        $inventoryId=(int)($usage['inventory_item_id']??0);
        if($inventoryId>0&&in_array((string)$usage['usage_type'],['medication_administered','medicine_dispense','consumable'],true)){
            $remaining=round((float)$usage['quantity'],2);
            $lots=$pdo->prepare('SELECT id,quantity_remaining,unit_cost FROM `' . table('inventory_lots') . '` WHERE item_id=? AND branch_id=? AND status="active" AND quantity_remaining>0 AND (expiry_date IS NULL OR expiry_date>=CURDATE()) ORDER BY COALESCE(expiry_date,"9999-12-31"),received_at,id FOR UPDATE');
            $lots->execute([$inventoryId,$branchId]);
            foreach($lots->fetchAll() as $lot){
                if($remaining<=0.001)break;
                $take=min($remaining,(float)$lot['quantity_remaining']);
                inventory_post_movement($pdo,[
                    'item_id'=>$inventoryId,'lot_id'=>(int)$lot['id'],'branch_id'=>$branchId,'transaction_type'=>'issue','quantity'=>-$take,
                    'unit_cost'=>$lot['unit_cost']??null,'reference_type'=>'clinical_usage','reference_id'=>(string)$usageId,
                    'notes'=>'Confirmed at cashier for encounter #'.(int)$usage['encounter_id'].' · '.(string)$usage['description'],
                ]);
                $remaining=round($remaining-$take,2);
            }
            if($remaining>0.001)throw new RuntimeException((string)$usage['description'].' no longer has enough unexpired stock. Ask Inventory to correct the lot balance before billing.');
        }
        $update=$pdo->prepare('UPDATE `' . table('clinical_usage_items') . '` SET fulfillment_status="confirmed",confirmed_by=?,confirmed_at=NOW(),updated_at=NOW() WHERE id=? AND fulfillment_status="requested"');
        $update->execute([$userId,$usageId]);
        if($update->rowCount()!==1)throw new RuntimeException('A clinical usage item changed before confirmation completed.');
    }
}

function cashier_invoice_has_retail_products(PDO $pdo, int $invoiceId): bool
{
    if ($invoiceId < 1 || !schema_column_exists(table('invoice_items'),'source_type')) return false;
    $stmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('invoice_items') . '` WHERE invoice_id=? AND source_type="retail_product"');
    $stmt->execute([$invoiceId]);
    return (int)$stmt->fetchColumn()>0;
}

function cashier_fulfill_retail_products(PDO $pdo, int $invoiceId, int $branchId, string $invoiceNo): void
{
    if ($invoiceId < 1 || $branchId < 1) return;
    if (!schema_column_exists(table('inventory_items'),'is_retail_product') || !schema_column_exists(table('inventory_items'),'selling_price')) {
        throw new RuntimeException('Apply Version 0.7.6.2 before completing a retail product sale.');
    }
    $stmt=$pdo->prepare('SELECT id,item_code,description,quantity FROM `' . table('invoice_items') . '` WHERE invoice_id=? AND source_type="retail_product" ORDER BY id FOR UPDATE');
    $stmt->execute([$invoiceId]);
    foreach ($stmt->fetchAll() as $line) {
        $lineId=(int)$line['id'];
        $already=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_transactions') . '` WHERE reference_type="retail_sale_item" AND reference_id=? AND status="posted"');
        $already->execute([(string)$lineId]);
        if ((int)$already->fetchColumn()>0) continue;
        if (!preg_match('/^RET-(\d+)$/',(string)$line['item_code'],$match)) throw new RuntimeException('Retail invoice line #' . $lineId . ' has an invalid inventory reference.');
        $itemId=(int)$match[1];
        $item=$pdo->prepare('SELECT id,name,status,is_retail_product FROM `' . table('inventory_items') . '` WHERE id=? AND status="active" AND is_retail_product=1 AND (branch_id=? OR branch_id IS NULL) FOR UPDATE');
        $item->execute([$itemId,$branchId]);
        if (!$item->fetch()) throw new RuntimeException((string)$line['description'] . ' is no longer an active retail product for this branch.');
        $remaining=round((float)$line['quantity'],2);
        $lots=$pdo->prepare('SELECT id,quantity_remaining,unit_cost FROM `' . table('inventory_lots') . '` WHERE item_id=? AND branch_id=? AND status="active" AND quantity_remaining>0 AND (expiry_date IS NULL OR expiry_date>=CURDATE()) ORDER BY CASE WHEN expiry_date IS NULL THEN 1 ELSE 0 END,expiry_date,id FOR UPDATE');
        $lots->execute([$itemId,$branchId]);
        foreach ($lots->fetchAll() as $lot) {
            if ($remaining<=0.001) break;
            $take=min($remaining,round((float)$lot['quantity_remaining'],2));
            if ($take<=0) continue;
            inventory_post_movement($pdo,[
                'item_id'=>$itemId,'lot_id'=>(int)$lot['id'],'branch_id'=>$branchId,'transaction_type'=>'issue','quantity'=>-$take,
                'unit_cost'=>$lot['unit_cost'],'reference_type'=>'retail_sale_item','reference_id'=>(string)$lineId,
                'notes'=>'Retail sale on invoice ' . $invoiceNo . ': ' . (string)$line['description'],
            ]);
            $remaining=round($remaining-$take,2);
        }
        if ($remaining>0.001) throw new RuntimeException((string)$line['description'] . ' no longer has enough unexpired stock. The payment was not posted; ask Inventory to correct or receive stock.');
    }
}

function cashier_posted_items(PDO $pdo, int $patientId, int $branchId, int $encounterId, int $invoiceId, bool $allowManual): array
{
    $descriptions = (array)($_POST['item_description'] ?? []);
    $quantities = (array)($_POST['item_quantity'] ?? []);
    $prices = (array)($_POST['item_unit_price'] ?? []);
    $serviceIds = (array)($_POST['item_service_id'] ?? []);
    $planIds = (array)($_POST['item_treatment_plan_item_id'] ?? []);
    $procedureIds = (array)($_POST['item_procedure_id'] ?? []);
    $usageIds = (array)($_POST['item_clinical_usage_id'] ?? []);
    $inventoryIds = (array)($_POST['item_inventory_id'] ?? []);
    $items = [];
    $seenProcedures = [];
    $seenUsage = [];
    $seenInventory = [];

    foreach ($descriptions as $index => $postedDescription) {
        if (count($items) >= 50) break;
        $serviceId = (int)($serviceIds[$index] ?? 0);
        $procedureId = (int)($procedureIds[$index] ?? 0);
        $usageId = (int)($usageIds[$index] ?? 0);
        $inventoryId = (int)($inventoryIds[$index] ?? 0);
        $quantity = round((float)($quantities[$index] ?? 1), 2);
        $itemNotes = null;
        $itemAuthorizedBy = null;
        $feeStatus = null;
        $itemCode = null;
        if ($quantity <= 0 || $quantity > 999) throw new RuntimeException('Each POS quantity must be greater than zero.');

        if ($inventoryId > 0) {
            if (isset($seenInventory[$inventoryId])) throw new RuntimeException('A retail product cannot be added twice. Change the quantity on the existing product line instead.');
            $seenInventory[$inventoryId] = true;
            if (!schema_column_exists(table('inventory_items'),'is_retail_product') || !schema_column_exists(table('inventory_items'),'selling_price')) throw new RuntimeException('Apply Version 0.7.6.2 before billing retail products.');
            $stmt=$pdo->prepare('SELECT i.id,i.sku,i.name,i.selling_price,bs.current_stock FROM `' . table('inventory_items') . '` i JOIN `' . table('inventory_branch_stocks') . '` bs ON bs.item_id=i.id AND bs.branch_id=? WHERE i.id=? AND i.status="active" AND i.is_retail_product=1 AND i.selling_price>0 AND (i.branch_id=? OR i.branch_id IS NULL) LIMIT 1');
            $stmt->execute([$branchId,$inventoryId,$branchId]);
            $row=$stmt->fetch();
            if(!$row)throw new RuntimeException('A submitted retail product is inactive, unavailable, or has no selling price for this branch.');
            if($quantity>(float)$row['current_stock']+0.001)throw new RuntimeException((string)$row['name'].' has only '.(string)$row['current_stock'].' available. Reduce the quantity or ask Inventory to receive stock.');
            $description=trim((string)$row['name']);$price=round((float)$row['selling_price'],2);$serviceId=null;$procedureId=0;$usageId=0;$planId=null;$sourceType='retail_product';$itemCode='RET-'.$inventoryId;
        } elseif ($usageId > 0) {
            if ($encounterId < 1) throw new RuntimeException('A clinical medicine, material, equipment, or diagnostic charge must remain linked to its finalized encounter.');
            if (isset($seenUsage[$usageId])) throw new RuntimeException('A clinical usage item cannot be billed twice on the same invoice.');
            $seenUsage[$usageId]=true;
            $stmt=$pdo->prepare('SELECT cu.*,i.sku FROM `' . table('clinical_usage_items') . '` cu LEFT JOIN `' . table('inventory_items') . '` i ON i.id=cu.inventory_item_id WHERE cu.id=? AND cu.patient_id=? AND cu.branch_id=? AND cu.encounter_id=? AND cu.billing_mode="bill_separately" AND cu.line_total>0 AND cu.fulfillment_status IN ("requested","confirmed") LIMIT 1');
            $stmt->execute([$usageId,$patientId,$branchId,$encounterId]);
            $row=$stmt->fetch();
            if(!$row)throw new RuntimeException('A submitted clinical medicine or material charge is no longer authorized for this encounter.');
            $description=trim((string)$row['description']);
            $quantity=round((float)$row['quantity'],2);
            $price=round((float)$row['unit_price'],2);
            $procedureId=0;$serviceId=null;$planId=null;
            $sourceType=match((string)$row['usage_type']){
                'medicine_dispense'=>'dispensed_medicine',
                'equipment'=>'equipment_service',
                'diagnostic_service'=>'diagnostic_service',
                default=>'clinical_supply',
            };
        } elseif ($procedureId > 0) {
            if ($encounterId < 1) throw new RuntimeException('A clinical procedure must remain linked to its finalized encounter. Reload the cashier register.');
            if (isset($seenProcedures[$procedureId])) throw new RuntimeException('A completed procedure cannot be billed twice on the same invoice.');
            $seenProcedures[$procedureId] = true;
            $procedureCharge = cashier_procedure_charge_fragments('p');
            $stmt = $pdo->prepare(
                'SELECT p.*,' . $procedureCharge['service_select'] . ','
                . $procedureCharge['name_expression'] . ' canonical_name,' . $procedureCharge['fee_expression'] . ' authorized_fee,'
                . $procedureCharge['fee_status_select'] . ',' . $procedureCharge['no_charge_select'] . ','
                . $procedureCharge['authorized_by_select'] . ',' . $procedureCharge['authorized_at_select'] . ','
                . $procedureCharge['billing_status_select'] . ' '
                . 'FROM `' . table('procedures') . '` p '
                . $procedureCharge['plan_join']
                . $procedureCharge['service_join']
                . 'WHERE p.id=? AND p.patient_id=? AND p.status="completed" AND (p.branch_id=? OR p.branch_id IS NULL) LIMIT 1'
            );
            $stmt->execute([$procedureId, $patientId, $branchId]);
            $row = $stmt->fetch();
            if (!$row) throw new RuntimeException('A submitted clinical charge is not a completed procedure for this patient.');
            if ($encounterId > 0 && (int)$row['encounter_id'] !== $encounterId) throw new RuntimeException('A clinical charge belongs to a different encounter.');
            clinical_billing_assert_procedure($row);
            $feeStatus = clinical_billing_procedure_fee_status($row);
            $price = $feeStatus === 'no_charge' ? 0.0 : round((float)$row['authorized_fee'], 2);
            $description = trim((string)($row['canonical_name'] ?? $row['procedure_name']) . (!empty($row['tooth_region']) ? ' · ' . (string)$row['tooth_region'] : ''));
            $quantity = 1.0;
            $sourceType = 'clinical_procedure';
            $serviceId = (int)$row['service_id'] ?: null;
            $planId = (int)$row['treatment_plan_item_id'] ?: null;
            $itemNotes = $feeStatus === 'no_charge' ? 'Authorized no-charge: ' . trim((string)$row['no_charge_reason']) : null;
            $itemAuthorizedBy = (int)($row['fee_authorized_by'] ?? 0) ?: null;
        } elseif ($serviceId > 0) {
            if ($encounterId > 0 && !$allowManual) {
                throw new RuntimeException('A cashier cannot add catalogue services to a clinical encounter. The invoice must contain only the Dentist-finalized procedures, or an Administrator must document the additional authorised charge.');
            }
            $stmt = $pdo->prepare(
                'SELECT id,name,standard_fee,fee_status FROM `' . table('service_catalog') . '` '
                . 'WHERE id=? AND is_active=1 AND fee_status="billable" AND standard_fee>0 AND (branch_id=? OR branch_id IS NULL) LIMIT 1'
            );
            $stmt->execute([$serviceId, $branchId]);
            $row = $stmt->fetch();
            if (!$row) throw new RuntimeException('A submitted service is inactive or unavailable for this branch.');
            $price = round((float)$row['standard_fee'], 2);
            if ($price <= 0) throw new RuntimeException('Set the standard fee before billing ' . (string)$row['name'] . '.');
            $description = trim((string)$row['name']);
            $planId = null;
            $sourceType = 'service_catalog';
        } else {
            if (!$allowManual) throw new RuntimeException('Cashiers cannot create or price custom charges. Ask an Administrator to add an authorized manual charge or service-catalog item.');
            $description = trim((string)$postedDescription);
            $price = round((float)($prices[$index] ?? 0), 2);
            if ($description === '') throw new RuntimeException('Every manual charge requires a description.');
            if ($price <= 0 || $price > 99999999) throw new RuntimeException('Enter a valid positive unit price for ' . $description . '.');
            $planId = (int)($planIds[$index] ?? 0) ?: null;
            $sourceType = 'manual_charge';
        }

        $gross = round($quantity * $price, 2);
        $sourcePayload = [
            'source_type'=>$sourceType,'service_id'=>$serviceId,'procedure_id'=>$procedureId ?: null,'clinical_usage_item_id'=>$usageId ?: null,'inventory_item_id'=>$inventoryId ?: null,
            'treatment_plan_item_id'=>$planId,'description'=>$description,'quantity'=>$quantity,'unit_price'=>$price,
            'fee_status'=>$feeStatus,
        ];
        $items[] = $sourcePayload + [
            'notes'=>$itemNotes,
            'source_authorized_by'=>$itemAuthorizedBy,
            'item_code'=>$itemCode ?: ($usageId > 0 ? 'USE-' . $usageId : ($procedureId > 0 ? 'PROC-' . $procedureId : ($serviceId ? 'SVC-' . $serviceId : 'MANUAL'))),
            'gross_amount'=>$gross,
            'source_hash'=>hash('sha256', json_encode($sourcePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
        ];
    }

    if (!$items) throw new RuntimeException('Add at least one authorized item to the register.');
    $required = cashier_required_procedure_ids($pdo, $encounterId, $invoiceId);
    $posted = array_keys($seenProcedures);
    sort($required); sort($posted);
    if ($required !== $posted) {
        throw new RuntimeException('All completed procedures for this encounter must be billed together. Reload the register to restore the authorized clinical charges.');
    }
    $requiredUsage=cashier_required_usage_ids($pdo,$encounterId,$invoiceId);
    $postedUsage=array_keys($seenUsage);sort($requiredUsage);sort($postedUsage);
    if($requiredUsage!==$postedUsage)throw new RuntimeException('All separately billable medicines, materials, equipment, and diagnostic items from this encounter must be reviewed together. Reload the register.');
    return $items;
}

function cashier_totals(array $items, ?array $discountAuthorization): array
{
    $subtotal = round(array_sum(array_column($items, 'gross_amount')), 2);
    $discountType = 'none';
    $discountValue = 0.0;
    $discount = 0.0;
    $reason = null;
    if ($discountAuthorization) {
        $discountType = (string)$discountAuthorization['discount_type'];
        $discountValue = round((float)$discountAuthorization['discount_value'], 2);
        if ($discountType === 'percent') $discount = round($subtotal * min(100, $discountValue) / 100, 2);
        if ($discountType === 'fixed') $discount = min($subtotal, $discountValue);
        $reason = (string)$discountAuthorization['reason'];
    }
    $total = max(0, round($subtotal - $discount, 2));
    return [$subtotal, $discount, $total, $discountType, $discountValue, $reason];
}

function cashier_save_items(PDO $pdo, int $invoiceId, array $items, float $discountAmount, int $userId): string
{
    $pdo->prepare('DELETE FROM `' . table('invoice_items') . '` WHERE invoice_id=?')->execute([$invoiceId]);
    $subtotal = max(0.01, array_sum(array_column($items, 'gross_amount')));
    $insert = $pdo->prepare(
        'INSERT INTO `' . table('invoice_items') . '` '
        . '(invoice_id,service_id,treatment_plan_item_id,procedure_id,clinical_usage_item_id,source_type,item_code,description,quantity,unit_price,gross_amount,discount_amount,net_amount,notes,source_authorized_by,source_authorized_at,source_hash,created_by,created_at) '
        . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,?,NOW())'
    );
    $remainingDiscount = $discountAmount;
    $invoiceHashPayload = [];
    foreach ($items as $index => $item) {
        $lineDiscount = $index === array_key_last($items)
            ? $remainingDiscount
            : min($remainingDiscount, round($discountAmount * ((float)$item['gross_amount'] / $subtotal), 2));
        $lineDiscount = min((float)$item['gross_amount'], max(0, $lineDiscount));
        $remainingDiscount = round($remainingDiscount - $lineDiscount, 2);
        $net = max(0, round((float)$item['gross_amount'] - $lineDiscount, 2));
        $insert->execute([
            $invoiceId,$item['service_id'],$item['treatment_plan_item_id'],$item['procedure_id'],$item['clinical_usage_item_id']??null,$item['source_type'],$item['item_code'],
            $item['description'],$item['quantity'],$item['unit_price'],$item['gross_amount'],$lineDiscount,$net,$item['notes']??null,$item['source_authorized_by']??($userId ?: null),$item['source_hash'],$userId ?: null,
        ]);
        $invoiceHashPayload[] = [$item['source_hash'],$lineDiscount,$net];
    }
    return hash('sha256', json_encode($invoiceHashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}

$paymentReferenceLock = null;
function cashier_reauthorize_legacy_invoice(PDO $pdo, array $invoice, int $branchId, int $userId, string $reason, string $evidence): string
{
    if (trim((string)($invoice['charge_source_hash'] ?? '')) !== '') {
        throw new RuntimeException('This invoice already has a verified charge fingerprint.');
    }
    if (!in_array((string)$invoice['status'], ['unpaid','partial'], true)) {
        throw new RuntimeException('Only an unpaid or partially paid legacy invoice can be reviewed for continued collection.');
    }
    $stmt = $pdo->prepare('SELECT * FROM `' . table('invoice_items') . '` WHERE invoice_id=? ORDER BY id FOR UPDATE');
    $stmt->execute([(int)$invoice['id']]);
    $items = $stmt->fetchAll();
    if (!$items) throw new RuntimeException('The legacy invoice has no line items to review.');

    $grossTotal = 0.0;
    $discountTotal = 0.0;
    $netTotal = 0.0;
    $update = $pdo->prepare(
        'UPDATE `' . table('invoice_items') . '` SET source_type=?,source_authorized_by=?,source_authorized_at=NOW(),source_hash=? WHERE id=? AND invoice_id=?'
    );
    foreach ($items as $item) {
        $quantity = round((float)$item['quantity'], 2);
        $unitPrice = round((float)$item['unit_price'], 2);
        $gross = round((float)$item['gross_amount'], 2);
        $discount = round((float)$item['discount_amount'], 2);
        $net = round((float)$item['net_amount'], 2);
        if ($quantity <= 0 || $unitPrice < 0 || abs($gross - round($quantity * $unitPrice, 2)) > 0.01) {
            throw new RuntimeException('Legacy invoice line #' . (int)$item['id'] . ' fails quantity and unit-price reconciliation.');
        }
        if ($discount < 0 || $discount > $gross + 0.01 || abs($net - round($gross - $discount, 2)) > 0.01) {
            throw new RuntimeException('Legacy invoice line #' . (int)$item['id'] . ' fails discount and net-amount reconciliation.');
        }

        $sourceType = (string)($item['source_type'] ?? 'legacy_manual');
        $procedureId = (int)($item['procedure_id'] ?? 0);
        $serviceId = (int)($item['service_id'] ?? 0);
        if ($procedureId > 0) {
            $sourceType = 'clinical_procedure';
            $source = $pdo->prepare(
                'SELECT id FROM `' . table('procedures') . '` WHERE id=? AND patient_id=? AND status="completed" '
                . 'AND (branch_id=? OR branch_id IS NULL) AND (?=0 OR encounter_id=?) LIMIT 1'
            );
            $source->execute([$procedureId,(int)$invoice['patient_id'],$branchId,(int)($invoice['encounter_id'] ?? 0),(int)($invoice['encounter_id'] ?? 0)]);
            if (!$source->fetchColumn()) throw new RuntimeException('Legacy invoice line #' . (int)$item['id'] . ' does not link to a completed procedure for this encounter.');
        } elseif ($serviceId > 0) {
            $sourceType = 'service_catalog';
            $source = $pdo->prepare('SELECT id FROM `' . table('service_catalog') . '` WHERE id=? AND (branch_id=? OR branch_id IS NULL) LIMIT 1');
            $source->execute([$serviceId,$branchId]);
            if (!$source->fetchColumn()) throw new RuntimeException('Legacy invoice line #' . (int)$item['id'] . ' references an unknown service-catalog item.');
        } else {
            $sourceType = 'manual_charge';
        }
        $payload = [
            'source_type'=>$sourceType,
            'service_id'=>$serviceId ?: null,
            'procedure_id'=>$procedureId ?: null,
            'treatment_plan_item_id'=>(int)($item['treatment_plan_item_id'] ?? 0) ?: null,
            'description'=>trim((string)$item['description']),
            'quantity'=>$quantity,
            'unit_price'=>$unitPrice,
        ];
        $sourceHash = hash('sha256', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $update->execute([$sourceType,$userId ?: null,$sourceHash,(int)$item['id'],(int)$invoice['id']]);
        if ($update->rowCount() !== 1) throw new RuntimeException('A legacy invoice line changed during Administrator review.');
        $grossTotal = round($grossTotal + $gross, 2);
        $discountTotal = round($discountTotal + $discount, 2);
        $netTotal = round($netTotal + $net, 2);
    }
    if (abs($grossTotal - (float)$invoice['subtotal']) > 0.01
        || abs($discountTotal - (float)$invoice['discount_amount']) > 0.01
        || abs($netTotal - (float)$invoice['amount']) > 0.01) {
        throw new RuntimeException('Legacy invoice totals do not reconcile with the reviewed line items. Correct the source record instead of approving it.');
    }
    if ((float)$invoice['paid_amount'] > $netTotal + 0.01) {
        throw new RuntimeException('Legacy invoice payments exceed the reviewed charge total.');
    }
    $hash = revenue_invoice_charge_hash($pdo, (int)$invoice['id']);
    $invoiceUpdate = $pdo->prepare(
        'UPDATE `' . table('invoices') . '` SET charge_source_hash=?,charge_locked_at=COALESCE(charge_locked_at,NOW()),charge_locked_by=?,legacy_charge_review_reason=?,legacy_charge_review_evidence=?,legacy_charge_reviewed_by=?,legacy_charge_reviewed_at=NOW(),updated_at=NOW() '
        . 'WHERE id=? AND branch_id=? AND charge_source_hash IS NULL'
    );
    $invoiceUpdate->execute([$hash,$userId,$reason,$evidence,$userId,(int)$invoice['id'],$branchId]);
    if ($invoiceUpdate->rowCount() !== 1) throw new RuntimeException('The legacy invoice changed before review approval could be saved.');
    return $hash;
}

function cashier_legacy_breakdown_placeholder(float $invoiceTotal): string
{
    $invoiceTotal = max(0.0, round($invoiceTotal, 2));
    return 'Actual service from old chart | 1 | ' . number_format($invoiceTotal, 2, '.', '');
}

function cashier_parse_legacy_breakdown(string $raw): array
{
    $rows = [];
    foreach (preg_split('/\R/u', trim($raw)) ?: [] as $lineNo => $line) {
        $line = trim($line);
        if ($line === '') continue;
        $parts = array_map('trim', explode('|', $line));
        if (count($parts) !== 3) {
            throw new RuntimeException('Breakdown line ' . ($lineNo + 1) . ' must use: Description | Quantity | Unit price.');
        }
        [$description,$quantityText,$unitPriceText] = $parts;
        $description = trim($description);
        if ($description === '' || revenue_receipt_description_is_generic($description)) {
            throw new RuntimeException('Breakdown line ' . ($lineNo + 1) . ' needs the actual service or procedure, not a generic balance description.');
        }
        if (!is_numeric(str_replace(',','',$quantityText)) || !is_numeric(str_replace(',','',$unitPriceText))) {
            throw new RuntimeException('Breakdown line ' . ($lineNo + 1) . ' has an invalid quantity or unit price.');
        }
        $quantity = round((float)str_replace(',','',$quantityText), 2);
        $unitPrice = round((float)str_replace(',','',$unitPriceText), 2);
        if ($quantity <= 0 || $quantity > 10000 || $unitPrice < 0) {
            throw new RuntimeException('Breakdown line ' . ($lineNo + 1) . ' has an invalid quantity or unit price.');
        }
        $gross = round($quantity * $unitPrice, 2);
        $rows[] = [
            'description'=>mb_substr($description,0,255),
            'quantity'=>$quantity,
            'unit_price'=>$unitPrice,
            'gross_amount'=>$gross,
        ];
    }
    if (!$rows) throw new RuntimeException('Enter at least one actual service or procedure in the historical breakdown.');
    if (count($rows) > 100) throw new RuntimeException('A single invoice cannot contain more than 100 reconstructed lines.');
    return $rows;
}

/**
 * Replace an imported/unitemized opening balance with an Administrator-reviewed
 * historical breakdown. This does not change the invoice total or any payment;
 * it only restores the service-level detail needed for a meaningful receipt.
 */
function cashier_itemize_legacy_invoice(PDO $pdo, array $invoice, int $branchId, int $userId, array $lines, string $reason, string $evidence): array
{
    if (!schema_table_exists(table('invoice_items'))) throw new RuntimeException('Invoice itemization is not installed.');
    if ((int)$invoice['branch_id'] !== $branchId) throw new RuntimeException('This invoice is outside the active branch.');
    if (in_array((string)$invoice['status'], ['voided','refunded','written_off'], true)) {
        throw new RuntimeException('A voided, refunded, or written-off invoice cannot be reconstructed.');
    }

    $existingStmt = $pdo->prepare('SELECT * FROM `' . table('invoice_items') . '` WHERE invoice_id=? ORDER BY id FOR UPDATE');
    $existingStmt->execute([(int)$invoice['id']]);
    $existing = $existingStmt->fetchAll();
    foreach ($existing as $item) {
        $protected = (int)($item['procedure_id'] ?? 0) > 0
            || (int)($item['treatment_plan_item_id'] ?? 0) > 0
            || (int)($item['clinical_usage_item_id'] ?? 0) > 0
            || (int)($item['inhouse_checkup_result_id'] ?? 0) > 0
            || (int)($item['service_id'] ?? 0) > 0;
        if ($protected || !revenue_receipt_description_is_generic((string)($item['description'] ?? ''))) {
            throw new RuntimeException('This invoice already contains a protected or specific billing line. Correct its clinical source instead of reconstructing it as a legacy balance.');
        }
    }

    $total = round(array_sum(array_column($lines, 'gross_amount')), 2);
    $invoiceTotal = round((float)$invoice['amount'], 2);
    if (abs($total - $invoiceTotal) > 0.01) {
        throw new RuntimeException('The reconstructed breakdown totals ₱' . number_format($total,2) . ', but the invoice total is ₱' . number_format($invoiceTotal,2) . '. The totals must match exactly.');
    }

    $pdo->prepare('DELETE FROM `' . table('invoice_items') . '` WHERE invoice_id=?')->execute([(int)$invoice['id']]);
    $insert = $pdo->prepare(
        'INSERT INTO `' . table('invoice_items') . '` '
        . '(invoice_id,source_type,item_code,description,quantity,unit_price,gross_amount,discount_amount,net_amount,notes,source_authorized_by,source_authorized_at,source_hash,created_by,created_at) '
        . 'VALUES (? ,"legacy_manual",?,?,?,?,?,0,?,?,?,?,?, ?,NOW())'
    );
    $descriptions = [];
    foreach ($lines as $index => $line) {
        $payload = [
            'source_type'=>'legacy_manual',
            'invoice_id'=>(int)$invoice['id'],
            'sequence'=>$index + 1,
            'description'=>$line['description'],
            'quantity'=>$line['quantity'],
            'unit_price'=>$line['unit_price'],
            'gross_amount'=>$line['gross_amount'],
            'evidence'=>$evidence,
        ];
        $hash = hash('sha256', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $insert->execute([
            (int)$invoice['id'],
            'LEGACY-' . str_pad((string)($index + 1),3,'0',STR_PAD_LEFT),
            $line['description'],$line['quantity'],$line['unit_price'],$line['gross_amount'],$line['gross_amount'],
            'Historical breakdown reconstructed from: ' . mb_substr($evidence,0,350),
            $userId ?: null,date('Y-m-d H:i:s'),$hash,$userId ?: null,
        ]);
        $descriptions[] = $line['description'];
    }

    $invoiceHash = revenue_invoice_charge_hash($pdo, (int)$invoice['id']);
    $summary = implode('; ', array_slice($descriptions,0,4));
    if (count($descriptions) > 4) $summary .= '; +' . (count($descriptions) - 4) . ' more';
    $update = $pdo->prepare(
        'UPDATE `' . table('invoices') . '` SET description=?,subtotal=?,discount_type="none",discount_value=0,discount_amount=0,tax_amount=0,amount=?,charge_source_hash=?,charge_locked_at=COALESCE(charge_locked_at,NOW()),charge_locked_by=?,legacy_charge_review_reason=?,legacy_charge_review_evidence=?,legacy_charge_reviewed_by=?,legacy_charge_reviewed_at=NOW(),updated_at=NOW() WHERE id=? AND branch_id=?'
    );
    $update->execute([$summary ?: 'Reviewed historical dental charges',$invoiceTotal,$invoiceTotal,$invoiceHash,$userId,$reason,$evidence,$userId,(int)$invoice['id'],$branchId]);
    if ($update->rowCount() !== 1) throw new RuntimeException('The invoice changed before the historical breakdown could be saved.');

    $rebuilt = revenue_rebuild_payment_allocations_for_invoice($pdo, (int)$invoice['id']);
    return ['charge_source_hash'=>$invoiceHash,'line_count'=>count($lines),'payment_snapshots_rebuilt'=>$rebuilt];
}

$postedPaymentRequestToken = trim((string)($_POST['payment_request_token'] ?? ''));
$paymentRequestToken = preg_match('/^[a-f0-9]{64}$/', $postedPaymentRequestToken) ? $postedPaymentRequestToken : bin2hex(random_bytes(32));

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        verify_csrf();
        require_task_action('billing.manage');
        $action = (string)($_POST['action'] ?? 'pos_checkout');
        $cashierActions=['open_session','close_session','lock_transaction','cancel_transaction','return_billing_correction','call_cashier','pos_checkout','resend_receipt','complete_cashier','process_adjustment'];
        $supervisorActions=['create_window','review_session','supervisor_reconcile_stale_session','itemize_legacy_invoice','reauthorize_legacy_invoice','request_discount','review_discount','request_zero_charge','review_zero_charge','request_adjustment','review_adjustment'];
        if(in_array($action,$cashierActions,true)&&!$isCashierOperator){
            throw new RuntimeException('This transaction must be performed by Cashier, General Staff, or a signed-in Superuser operating the cashier workspace.');
        }
        if(in_array($action,$supervisorActions,true)&&!$isSupervisor){
            throw new RuntimeException('This finance-control action requires the signed-in Clinic Administrator or Superuser account.');
        }

        if ($action === 'create_window') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can add cashier windows.');
            if (!$posReady) throw new RuntimeException('Apply Version 0.5.9.8.14 before adding cashier windows.');
            $windowName=trim((string)($_POST['window_name']??''));
            $windowCode=strtoupper(trim((string)($_POST['window_code']??'')));
            if($windowName===''||$windowCode==='')throw new RuntimeException('Enter both the cashier window name and code.');
            if(!preg_match('/^[A-Z0-9_-]{1,40}$/',$windowCode))throw new RuntimeException('Window code may contain only letters, numbers, underscores, and hyphens.');
            $stmt=$pdo->prepare('INSERT INTO `' . table('cashier_windows') . '` (branch_id,window_code,window_name,is_active,created_by,created_at) VALUES (?,?,?,1,?,NOW())');
            $stmt->execute([$branchId,$windowCode,mb_substr($windowName,0,100),$userId]);
            audit('cashier_window_created','cashier_window',(int)$pdo->lastInsertId(),['window_code'=>$windowCode,'window_name'=>$windowName]);
            flash('success','Cashier window added.');
            header('Location: billing.php');exit;
        }

        if ($action === 'open_session') {
            if (!$posReady) throw new RuntimeException('Apply Version 0.5.9.8.14 before opening a cashier session.');
            $opening = round((float)($_POST['opening_cash'] ?? 0), 2);
            $windowId=(int)($_POST['window_id']??0);
            if ($opening < 0) throw new RuntimeException('Opening cash cannot be negative.');
            $window=revenue_cashier_window($pdo,$branchId,$windowId);
            $sessionLock = revenue_lock_name('cashier_session_open', $branchId);
            revenue_acquire_lock($pdo, $sessionLock);
            try {
                $pdo->beginTransaction();
                if (revenue_active_cashier_session($pdo, $branchId, $userId, true)) throw new RuntimeException('You already have an open cashier session.');
                $windowBusy=$pdo->prepare('SELECT id FROM `' . table('cashier_sessions') . '` WHERE branch_id=? AND window_id=? AND status="open" LIMIT 1 FOR UPDATE');
                $windowBusy->execute([$branchId,$windowId]);
                if($windowBusy->fetchColumn())throw new RuntimeException('This cashier window already has an open session. Choose another window.');
                $pdo->prepare('INSERT INTO `' . table('cashier_sessions') . '` (branch_id,cashier_user_id,window_id,window_name,status,opening_cash,opened_at) VALUES (?, ?, ?, ?, "open", ?, NOW())')
                    ->execute([$branchId, $userId, $windowId, (string)$window['window_name'], $opening]);
                $sessionId = (int)$pdo->lastInsertId();
                audit_required('cashier_session_opened', 'cashier_session', $sessionId, ['opening_cash'=>$opening,'window_id'=>$windowId,'window_name'=>$window['window_name']]);
                $pdo->commit();
            } finally {
                revenue_release_lock($pdo, $sessionLock);
            }
            flash('success', (string)$window['window_name'] . ' opened with ₱' . number_format($opening, 2) . ' opening cash.');
            header('Location: billing.php'); exit;
        }

        if ($action === 'close_session') {
            $counted = round((float)($_POST['counted_cash'] ?? -1), 2);
            if ($counted < 0) throw new RuntimeException('Enter the physical cash counted in the drawer.');
            $pdo->beginTransaction();
            $session = revenue_require_cashier_session($pdo, $branchId, $userId, true);
            if((int)($session['locked_patient_id']??0)>0)throw new RuntimeException('Complete or cancel the active patient transaction before closing this cashier window.');
            $totals = revenue_cashier_session_totals($pdo, (int)$session['id']);
            $expected = round((float)$session['opening_cash'] + $totals['cash_received'] - $totals['cash_returned'], 2);
            $variance = round($counted - $expected, 2);
            $status = abs($variance) <= 0.01 ? 'closed' : 'closed_pending_review';
            $sessionClose = $pdo->prepare(
                'UPDATE `' . table('cashier_sessions') . '` SET status=?,closed_at=NOW(),expected_cash=?,counted_cash=?,variance=?,close_notes=? WHERE id=? AND status="open"'
            );
            $sessionClose->execute([$status,$expected,$counted,$variance,trim((string)($_POST['close_notes'] ?? '')) ?: null,(int)$session['id']]);
            if ($sessionClose->rowCount() !== 1) throw new RuntimeException('The cashier session changed before it could be closed. Reload and reconcile again.');
            audit_required('cashier_session_closed', 'cashier_session', (int)$session['id'], $totals + ['expected_cash'=>$expected,'counted_cash'=>$counted,'variance'=>$variance,'status'=>$status]);
            $pdo->commit();
            flash($status === 'closed' ? 'success' : 'warning', $status === 'closed' ? 'Cashier session reconciled and closed.' : 'Cashier session closed with a variance and requires Administrator review.');
            header('Location: billing.php'); exit;
        }

        if ($action === 'review_session') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can review a cashier variance.');
            $sessionId = (int)($_POST['session_id'] ?? 0);
            $note = trim((string)($_POST['review_note'] ?? ''));
            if ($note === '') throw new RuntimeException('Enter the variance review note.');
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT * FROM `' . table('cashier_sessions') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$sessionId, $branchId]);
            $session = $stmt->fetch();
            if (!$session || (string)$session['status'] !== 'closed_pending_review') throw new RuntimeException('This session does not require review.');
            if ((int)$session['cashier_user_id'] === $userId) throw new RuntimeException('The cashier cannot review their own drawer variance.');
            $sessionReview = $pdo->prepare('UPDATE `' . table('cashier_sessions') . '` SET status="closed",reviewed_by=?,reviewed_at=NOW(),review_note=? WHERE id=? AND status="closed_pending_review"');
            $sessionReview->execute([$userId,$note,$sessionId]);
            if ($sessionReview->rowCount() !== 1) throw new RuntimeException('The cashier-session review state changed. Reload before approving the variance.');
            audit_required('cashier_session_variance_reviewed', 'cashier_session', $sessionId, ['variance'=>$session['variance'],'note'=>$note]);
            $pdo->commit();
            flash('success', 'Cashier variance reviewed and session closed.');
            header('Location: ' . cashier_finance_return_url()); exit;
        }


        if ($action === 'supervisor_reconcile_stale_session') {
            if (!$isSupervisor) throw new RuntimeException('Only the signed-in Administrator or Superuser can reconcile another cashier\'s stale session.');
            $sessionId = (int)($_POST['session_id'] ?? 0);
            $counted = round((float)($_POST['counted_cash'] ?? -1), 2);
            $note = trim((string)($_POST['reconciliation_note'] ?? ''));
            if ($sessionId < 1) throw new RuntimeException('Select the stale cashier session to reconcile.');
            if ($counted < 0) throw new RuntimeException('Enter the physical cash counted or the verified drawer amount.');
            if (mb_strlen($note) < 10) throw new RuntimeException('Enter a reconciliation note of at least 10 characters.');

            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT cs.*,u.name cashier_name FROM `' . table('cashier_sessions') . '` cs LEFT JOIN `' . table('users') . '` u ON u.id=cs.cashier_user_id WHERE cs.id=? AND cs.branch_id=? FOR UPDATE');
            $stmt->execute([$sessionId,$branchId]);
            $session = $stmt->fetch();
            if (!$session) throw new RuntimeException('The cashier session was not found for this branch.');
            if ((string)$session['status'] !== 'open') throw new RuntimeException('This cashier session is no longer open. Reload Billing.');
            if (!empty($session['opened_at']) && strtotime((string)$session['opened_at']) > time() - 18 * 3600) {
                throw new RuntimeException('This session is not yet classified as stale. Ask the assigned cashier to close it normally.');
            }
            if ((int)($session['locked_patient_id'] ?? 0) > 0 || (int)($session['locked_queue_entry_id'] ?? 0) > 0) {
                throw new RuntimeException('This stale session still owns a patient transaction lock. Resolve or cancel that focused transaction before drawer reconciliation.');
            }

            $totals = revenue_cashier_session_totals($pdo, $sessionId);
            $expected = round((float)$session['opening_cash'] + (float)$totals['cash_received'] - (float)$totals['cash_returned'], 2);
            $variance = round($counted - $expected, 2);
            $status = abs($variance) <= 0.01 ? 'closed' : 'closed_pending_review';
            $closeNote = 'Supervisor stale-session reconciliation: ' . $note;
            $update = $pdo->prepare('UPDATE `' . table('cashier_sessions') . '` SET status=?,closed_at=NOW(),expected_cash=?,counted_cash=?,variance=?,close_notes=? WHERE id=? AND branch_id=? AND status="open"');
            $update->execute([$status,$expected,$counted,$variance,$closeNote,$sessionId,$branchId]);
            if ($update->rowCount() !== 1) throw new RuntimeException('The cashier session changed before reconciliation was saved. Reload Billing.');
            audit_required('cashier_stale_session_reconciled','cashier_session',$sessionId,$totals + [
                'cashier_user_id'=>(int)($session['cashier_user_id'] ?? 0),
                'cashier_name'=>(string)($session['cashier_name'] ?? ''),
                'expected_cash'=>$expected,
                'counted_cash'=>$counted,
                'variance'=>$variance,
                'status'=>$status,
                'note'=>$note,
                'supervisor_user_id'=>$userId,
            ]);
            $pdo->commit();
            flash($status === 'closed' ? 'success' : 'warning', $status === 'closed'
                ? 'Stale cashier session reconciled and closed.'
                : 'Stale cashier session closed with a variance and moved to Cashier Exceptions for independent review.');
            header('Location: billing.php#stale-cashier-sessions'); exit;
        }

        if ($action === 'itemize_legacy_invoice') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can reconstruct an imported balance breakdown.');
            if (!$posReady) throw new RuntimeException('Apply the current finance updates before reconstructing a legacy invoice.');
            $invoiceId = (int)($_POST['invoice_id'] ?? 0);
            $reason = trim((string)($_POST['review_reason'] ?? ''));
            $evidence = trim((string)($_POST['evidence_reference'] ?? ''));
            $rawBreakdown = trim((string)($_POST['item_breakdown'] ?? ''));
            if (mb_strlen($reason) < 15) throw new RuntimeException('Enter a specific reconstruction reason of at least 15 characters.');
            if ($evidence === '') throw new RuntimeException('Enter the historical chart, receipt, ledger, or source-document reference.');
            $lines = cashier_parse_legacy_breakdown($rawBreakdown);
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$invoiceId,$branchId]);
            $invoice = $stmt->fetch();
            if (!$invoice) throw new RuntimeException('Legacy invoice not found for this branch.');
            $result = cashier_itemize_legacy_invoice($pdo,$invoice,$branchId,$userId,$lines,$reason,$evidence);
            audit_required('legacy_invoice_breakdown_reconstructed','invoice',$invoiceId,[
                'reason'=>$reason,'evidence_reference'=>$evidence,'line_count'=>$result['line_count'],
                'payment_snapshots_rebuilt'=>$result['payment_snapshots_rebuilt'],'charge_source_hash'=>$result['charge_source_hash'],
            ]);
            $pdo->commit();
            flash('success','The historical balance now has an itemized breakdown. Existing payment receipts were rebuilt without changing any payment amount.');
            header('Location: billing.php?view=exceptions&exception_type=legacy'); exit;
        }

        if ($action === 'reauthorize_legacy_invoice') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can review and re-authorize a legacy invoice.');
            if (!$posReady) throw new RuntimeException('Apply Version 0.5.9.7 before reviewing legacy charges.');
            $invoiceId = (int)($_POST['invoice_id'] ?? 0);
            $reason = trim((string)($_POST['review_reason'] ?? ''));
            $evidence = trim((string)($_POST['evidence_reference'] ?? ''));
            if (mb_strlen($reason) < 15) throw new RuntimeException('Enter a specific review reason of at least 15 characters.');
            if ($evidence === '') throw new RuntimeException('Enter the supporting source, chart, approval, or document reference used in the review.');
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$invoiceId,$branchId]);
            $invoice = $stmt->fetch();
            if (!$invoice) throw new RuntimeException('Legacy invoice not found for this branch.');
            $hash = cashier_reauthorize_legacy_invoice($pdo,$invoice,$branchId,$userId,$reason,$evidence);
            audit_required('legacy_invoice_charge_reauthorized','invoice',$invoiceId,['reason'=>$reason,'evidence_reference'=>$evidence,'charge_source_hash'=>$hash]);
            $pdo->commit();
            flash('success','Legacy invoice reviewed, reconciled, and fingerprinted. Further payment is now permitted.');
            header('Location: ' . cashier_finance_return_url('billing.php?invoice_id=' . $invoiceId . '&patient_id=' . (int)$invoice['patient_id'])); exit;
        }

        if ($action === 'request_discount') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator or Superuser can request or approve discounts. Cashier can only collect the payable amount shown.');
            $patientId = (int)($_POST['patient_id'] ?? 0);
            $queueId = (int)($_POST['queue_entry_id'] ?? 0);
            $type = (string)($_POST['discount_type'] ?? '');
            $value = round((float)($_POST['discount_value'] ?? 0), 2);
            $reason = trim((string)($_POST['reason'] ?? ''));
            if (!in_array($type, ['percent','fixed'], true) || $value <= 0) throw new RuntimeException('Enter a valid discount request.');
            if ($type === 'percent' && $value > 100) throw new RuntimeException('A percentage discount cannot exceed 100%.');
            if ($reason === '') throw new RuntimeException('Enter the policy, statutory basis, or business reason for the discount.');
            require_patient($patientId);
            $session = revenue_require_cashier_session($pdo,$branchId,$userId,true);
            revenue_assert_cashier_transaction($session,$patientId,$queueId);
            if ($queueId > 0) {
                $queueScope = $pdo->prepare('SELECT status FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=?');
                $queueScope->execute([$queueId,$patientId,$branchId]);
                $queueStatus = $queueScope->fetchColumn();
                if ($queueStatus === false || !in_array((string)$queueStatus,['waiting_billing','cashier_called'],true)) throw new RuntimeException('The selected visit is no longer in the cashier stage.');
            }
            $pendingDiscount = $pdo->prepare('SELECT COUNT(*) FROM `' . table('discount_authorizations') . '` WHERE branch_id=? AND patient_id=? AND status IN ("pending","approved") AND ((queue_entry_id IS NULL AND ?=0) OR queue_entry_id=?)');
            $pendingDiscount->execute([$branchId,$patientId,$queueId,$queueId]);
            if ((int)$pendingDiscount->fetchColumn() > 0) throw new RuntimeException('This patient and visit already has a pending or approved discount request. Use or resolve it before creating another.');
            $pdo->prepare(
                'INSERT INTO `' . table('discount_authorizations') . '` (branch_id,patient_id,queue_entry_id,discount_type,discount_value,reason,evidence_reference,status,requested_by,requested_at) '
                . 'VALUES (?,?,?,?,?,?,?,"pending",?,NOW())'
            )->execute([$branchId,$patientId,$queueId ?: null,$type,$value,$reason,trim((string)($_POST['evidence_reference'] ?? '')) ?: null,$userId]);
            $id = (int)$pdo->lastInsertId();
            audit('discount_authorization_requested', 'discount_authorization', $id, ['patient_id'=>$patientId,'type'=>$type,'value'=>$value]);
            flash('success', 'Discount request submitted for independent approval.');
            header('Location: billing.php?patient_id=' . $patientId . ($queueId ? '&queue_entry_id=' . $queueId : '')); exit;
        }

        if ($action === 'review_discount') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can review discount requests.');
            $id = (int)($_POST['authorization_id'] ?? 0);
            $decision = (string)($_POST['decision'] ?? '');
            if (!in_array($decision, ['approved','rejected'], true)) throw new RuntimeException('Select an approval decision.');
            $note = trim((string)($_POST['review_note'] ?? ''));
            if ($decision === 'rejected' && $note === '') throw new RuntimeException('Enter the rejection reason.');
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT * FROM `' . table('discount_authorizations') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$id, $branchId]);
            $row = $stmt->fetch();
            if (!$row || (string)$row['status'] !== 'pending') throw new RuntimeException('This discount request is no longer pending.');
            require_independent_actor([(int)$row['requested_by']], 'request and approve the same discount');
            if ($decision === 'approved' && !empty($row['queue_entry_id'])) {
                $queueCheck = $pdo->prepare('SELECT status FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=?');
                $queueCheck->execute([(int)$row['queue_entry_id'],(int)$row['patient_id'],$branchId]);
                $queueStatus = $queueCheck->fetchColumn();
                if ($queueStatus === false || !in_array((string)$queueStatus,['waiting_billing','cashier_called'],true)) throw new RuntimeException('This visit has left the cashier stage. Reject or expire the discount request instead of approving it.');
            }
            $discountReview = $pdo->prepare('UPDATE `' . table('discount_authorizations') . '` SET status=?,reviewed_by=?,reviewed_at=NOW(),review_note=? WHERE id=? AND status="pending"');
            $discountReview->execute([$decision,$userId,$note ?: null,$id]);
            if ($discountReview->rowCount() !== 1) throw new RuntimeException('The discount request changed before the review was saved.');
            audit_required('discount_authorization_reviewed', 'discount_authorization', $id, ['decision'=>$decision,'note'=>$note]);
            $pdo->commit();
            flash('success', 'Discount request ' . $decision . '.');
            header('Location: ' . cashier_finance_return_url()); exit;
        }

        if ($action === 'request_zero_charge') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator or Superuser can request a charge waiver. Cashier can only collect the payable amount shown.');
            $patientId = (int)($_POST['patient_id'] ?? 0);
            $queueId = (int)($_POST['queue_entry_id'] ?? 0);
            $category = (string)($_POST['category'] ?? '');
            $reason = trim((string)($_POST['reason'] ?? ''));
            $allowed = ['no_charge_followup','warranty_rework','package_included','staff_benefit','charity','waived_balance','other'];
            if ($patientId < 1 || $queueId < 1 || !in_array($category, $allowed, true)) throw new RuntimeException('Select a valid patient visit and zero-charge category.');
            if ($reason === '') throw new RuntimeException('Explain why this visit should close without a charge.');
            require_patient($patientId);
            $session = revenue_require_cashier_session($pdo,$branchId,$userId,true);
            revenue_assert_cashier_transaction($session,$patientId,$queueId);
            $stmt = $pdo->prepare('SELECT id FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=? AND status IN ("waiting_billing","cashier_called")');
            $stmt->execute([$queueId,$patientId,$branchId]);
            if (!$stmt->fetchColumn()) throw new RuntimeException('The selected visit is no longer in the cashier stage.');
            $pendingZero = $pdo->prepare('SELECT COUNT(*) FROM `' . table('zero_charge_authorizations') . '` WHERE branch_id=? AND patient_id=? AND queue_entry_id=? AND status IN ("pending","approved")');
            $pendingZero->execute([$branchId,$patientId,$queueId]);
            if ((int)$pendingZero->fetchColumn() > 0) throw new RuntimeException('This visit already has a pending or approved zero-charge request. Use or resolve it before creating another.');
            $pdo->prepare(
                'INSERT INTO `' . table('zero_charge_authorizations') . '` (branch_id,patient_id,queue_entry_id,category,reason,evidence_reference,status,requested_by,requested_at) '
                . 'VALUES (?,?,?,?,?,?,"pending",?,NOW())'
            )->execute([$branchId,$patientId,$queueId,$category,$reason,trim((string)($_POST['evidence_reference'] ?? '')) ?: null,$userId]);
            $id = (int)$pdo->lastInsertId();
            audit('zero_charge_authorization_requested', 'zero_charge_authorization', $id, ['queue_entry_id'=>$queueId,'category'=>$category]);
            flash('success', 'Zero-charge request submitted for independent approval.');
            header('Location: billing.php?queue_entry_id=' . $queueId); exit;
        }

        if ($action === 'review_zero_charge') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can review zero-charge requests.');
            $id = (int)($_POST['authorization_id'] ?? 0);
            $decision = (string)($_POST['decision'] ?? '');
            if (!in_array($decision, ['approved','rejected'], true)) throw new RuntimeException('Select an approval decision.');
            $note = trim((string)($_POST['review_note'] ?? ''));
            if ($decision === 'rejected' && $note === '') throw new RuntimeException('Enter the rejection reason.');
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT * FROM `' . table('zero_charge_authorizations') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$id,$branchId]);
            $row = $stmt->fetch();
            if (!$row || (string)$row['status'] !== 'pending') throw new RuntimeException('This zero-charge request is no longer pending.');
            require_independent_actor([(int)$row['requested_by']], 'request and approve the same zero-charge case');
            if ($decision === 'approved') {
                $queueCheck = $pdo->prepare('SELECT status FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=?');
                $queueCheck->execute([(int)$row['queue_entry_id'],(int)$row['patient_id'],$branchId]);
                $queueStatus = $queueCheck->fetchColumn();
                if ($queueStatus === false || !in_array((string)$queueStatus,['waiting_billing','cashier_called'],true)) throw new RuntimeException('This visit has left the cashier stage. Reject the zero-charge request instead of approving it.');
            }
            $zeroReview = $pdo->prepare('UPDATE `' . table('zero_charge_authorizations') . '` SET status=?,reviewed_by=?,reviewed_at=NOW(),review_note=? WHERE id=? AND status="pending"');
            $zeroReview->execute([$decision,$userId,$note ?: null,$id]);
            if ($zeroReview->rowCount() !== 1) throw new RuntimeException('The zero-charge request changed before the review was saved.');
            audit_required('zero_charge_authorization_reviewed', 'zero_charge_authorization', $id, ['decision'=>$decision,'note'=>$note]);
            $pdo->commit();
            flash('success', 'Zero-charge request ' . $decision . '.');
            header('Location: ' . cashier_finance_return_url()); exit;
        }

        if ($action === 'request_adjustment') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator or Superuser can request refunds, reversals, or payment adjustments.');
            $paymentId = (int)($_POST['payment_id'] ?? 0);
            $type = (string)($_POST['adjustment_type'] ?? '');
            $amount = round((float)($_POST['amount'] ?? 0), 2);
            $reason = trim((string)($_POST['reason'] ?? ''));
            if (!in_array($type, ['refund','reversal'], true) || $amount <= 0 || $reason === '') throw new RuntimeException('Complete the refund or reversal request.');
            $stmt = $pdo->prepare(
                'SELECT p.*,i.branch_id,i.patient_id,i.invoice_no,i.id invoice_id FROM `' . table('payments') . '` p '
                . 'JOIN `' . table('invoices') . '` i ON i.id=p.invoice_id WHERE p.id=? AND i.branch_id=? LIMIT 1'
            );
            $stmt->execute([$paymentId,$branchId]);
            $payment = $stmt->fetch();
            if (!$payment) throw new RuntimeException('Payment not found for this branch.');
            if ((string)($payment['source_type'] ?? 'patient') !== 'patient') throw new RuntimeException('Insurer remittances must be corrected through the insurance claim workflow, not the patient refund drawer.');
            $method = (string)$payment['method'];
            $reference = revenue_clean_payment_reference((string)($_POST['reference_no'] ?? ''));
            $returnReferenceRequired = revenue_payment_method_requires_reference($pdo, $method);
            if ($returnReferenceRequired && mb_strlen($reference) < 4) throw new RuntimeException('Enter the processor reference for the return to the original payment method.');
            if ($returnReferenceRequired) {
                $paymentReferenceLock = revenue_lock_name('return_reference', $branchId, $method, mb_strtolower($reference));
                revenue_acquire_lock($pdo, $paymentReferenceLock);
                $returnReference = $pdo->prepare('SELECT COUNT(*) FROM `' . table('payment_adjustments') . '` WHERE branch_id=? AND method=? AND reference_no=? AND status<>"rejected"');
                $returnReference->execute([$branchId,$method,$reference]);
                if ((int)$returnReference->fetchColumn() > 0) throw new RuntimeException('This return processor reference is already attached to another refund or reversal.');
            }
            $reservedStmt = $pdo->prepare('SELECT COALESCE(SUM(amount),0) FROM `' . table('payment_adjustments') . '` WHERE payment_id=? AND status IN ("requested","approved")');
            $reservedStmt->execute([$paymentId]);
            $reserved = round((float)$reservedStmt->fetchColumn(),2);
            $available = max(0,round((float)$payment['amount'] - (float)$payment['adjustment_total'] - $reserved, 2));
            if ($amount > $available + 0.001) throw new RuntimeException('The requested amount exceeds the unreserved payment balance of ₱' . number_format($available, 2) . '.');
            if ($type === 'reversal' && abs($amount - $available) > 0.001) throw new RuntimeException('A reversal must cancel the full remaining value of the original payment. Use Refund for a partial return.');
            $pdo->prepare(
                'INSERT INTO `' . table('payment_adjustments') . '` (branch_id,invoice_id,payment_id,adjustment_type,amount,method,reference_no,reason,status,requested_by,requested_at) '
                . 'VALUES (?,?,?,?,?,?,?,?,"requested",?,NOW())'
            )->execute([$branchId,(int)$payment['invoice_id'],$paymentId,$type,$amount,$method,$reference ?: null,$reason,$userId]);
            $id = (int)$pdo->lastInsertId();
            audit('payment_adjustment_requested', 'payment_adjustment', $id, ['payment_id'=>$paymentId,'type'=>$type,'amount'=>$amount]);
            if ($paymentReferenceLock !== null) {
                revenue_release_lock($pdo, $paymentReferenceLock);
                $paymentReferenceLock = null;
            }
            flash('success', ucfirst($type) . ' request submitted for independent approval.');
            header('Location: billing.php?invoice_id=' . (int)$payment['invoice_id'] . '&patient_id=' . (int)$payment['patient_id']); exit;
        }

        if ($action === 'review_adjustment') {
            if (!$isSupervisor) throw new RuntimeException('Only an Administrator can review refunds and reversals.');
            $id = (int)($_POST['adjustment_id'] ?? 0);
            $decision = (string)($_POST['decision'] ?? '');
            if (!in_array($decision, ['approved','rejected'], true)) throw new RuntimeException('Select an approval decision.');
            $note = trim((string)($_POST['review_note'] ?? ''));
            if ($decision === 'rejected' && $note === '') throw new RuntimeException('Enter the rejection reason.');
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT a.*,p.amount payment_amount,p.adjustment_total,p.source_type FROM `' . table('payment_adjustments') . '` a JOIN `' . table('payments') . '` p ON p.id=a.payment_id WHERE a.id=? AND a.branch_id=? FOR UPDATE');
            $stmt->execute([$id,$branchId]);
            $row = $stmt->fetch();
            if (!$row || (string)$row['status'] !== 'requested') throw new RuntimeException('This adjustment request is no longer pending.');
            require_independent_actor([(int)$row['requested_by']], 'request and approve the same refund or reversal');
            if ((string)($row['source_type'] ?? 'patient') !== 'patient') throw new RuntimeException('Insurer remittances cannot be approved through the patient refund workflow.');
            $reservedStmt = $pdo->prepare('SELECT COALESCE(SUM(amount),0) FROM `' . table('payment_adjustments') . '` WHERE payment_id=? AND id<>? AND status="approved"');
            $reservedStmt->execute([(int)$row['payment_id'],$id]);
            $reserved = round((float)$reservedStmt->fetchColumn(),2);
            $available = max(0,round((float)$row['payment_amount'] - (float)$row['adjustment_total'] - $reserved, 2));
            if ((float)$row['amount'] > $available + 0.001) throw new RuntimeException('The requested amount now exceeds the unreserved payment value. Reject it and create a corrected request.');
            if ((string)$row['adjustment_type'] === 'reversal' && abs((float)$row['amount'] - $available) > 0.001) throw new RuntimeException('The remaining payment changed; this reversal no longer cancels the full payment and must be recreated.');
            $adjustmentReview = $pdo->prepare('UPDATE `' . table('payment_adjustments') . '` SET status=?,approved_by=?,approved_at=NOW(),review_note=? WHERE id=? AND status="requested"');
            $adjustmentReview->execute([$decision,$userId,$note ?: null,$id]);
            if ($adjustmentReview->rowCount() !== 1) throw new RuntimeException('The payment-adjustment request changed before the review was saved.');
            audit_required('payment_adjustment_reviewed', 'payment_adjustment', $id, ['decision'=>$decision,'note'=>$note]);
            $pdo->commit();
            flash('success', 'Payment adjustment ' . $decision . '.');
            header('Location: ' . cashier_finance_return_url()); exit;
        }

        if ($action === 'process_adjustment') {
            $id = (int)($_POST['adjustment_id'] ?? 0);
            $pdo->beginTransaction();
            $session = revenue_require_cashier_session($pdo, $branchId, $userId, true);
            $stmt = $pdo->prepare('SELECT * FROM `' . table('payment_adjustments') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$id,$branchId]);
            $adjustment = $stmt->fetch();
            if (!$adjustment || (string)$adjustment['status'] !== 'approved') throw new RuntimeException('This refund or reversal is not approved for processing.');
            if ((int)$adjustment['approved_by'] === $userId) throw new RuntimeException('The approver cannot also process the refund or reversal.');
            $paymentStmt = $pdo->prepare('SELECT * FROM `' . table('payments') . '` WHERE id=? AND invoice_id=? FOR UPDATE');
            $paymentStmt->execute([(int)$adjustment['payment_id'],(int)$adjustment['invoice_id']]);
            $payment = $paymentStmt->fetch();
            if (!$payment) throw new RuntimeException('Original payment not found.');
            if ((string)($payment['source_type'] ?? 'patient') !== 'patient') throw new RuntimeException('Insurer remittances cannot be returned through the patient-payment workflow.');
            if ((string)$adjustment['method'] !== (string)$payment['method']) throw new RuntimeException('The approved return method no longer matches the original payment method.');
            $available = round((float)$payment['amount'] - (float)$payment['adjustment_total'], 2);
            if ((float)$adjustment['amount'] > $available + 0.001) throw new RuntimeException('The original payment no longer has enough unadjusted value.');
            if ((string)$adjustment['adjustment_type'] === 'reversal' && abs((float)$adjustment['amount'] - $available) > 0.001) throw new RuntimeException('A reversal must cancel the full remaining value of the original payment.');
            $adjustmentProcess = $pdo->prepare(
                'UPDATE `' . table('payment_adjustments') . '` SET status="processed",processed_by=?,processed_at=NOW(),cashier_session_id=? WHERE id=? AND status="approved"'
            );
            $adjustmentProcess->execute([$userId,(int)$session['id'],$id]);
            if ($adjustmentProcess->rowCount() !== 1) throw new RuntimeException('The refund or reversal changed before processing. Reload the approval queue.');
            revenue_recalculate_payment_status($pdo, (int)$adjustment['payment_id']);
            $invoiceState = revenue_recalculate_invoice($pdo, (int)$adjustment['invoice_id'], (string)$adjustment['adjustment_type']);
            audit_required('payment_adjustment_processed', 'payment_adjustment', $id, ['invoice_id'=>$adjustment['invoice_id'],'amount'=>$adjustment['amount'],'type'=>$adjustment['adjustment_type'],'invoice_status'=>$invoiceState['status']]);
            $pdo->commit();
            flash('success', ucfirst((string)$adjustment['adjustment_type']) . ' processed. The original payment remains preserved in the audit trail.');
            header('Location: ' . cashier_finance_return_url('billing.php?invoice_id=' . (int)$adjustment['invoice_id'])); exit;
        }

        if ($action === 'lock_transaction') {
            $patientId=(int)($_POST['patient_id']??0);$queueId=(int)($_POST['queue_entry_id']??0);$invoiceId=(int)($_POST['invoice_id']??0);$encounterId=(int)($_POST['encounter_id']??0);
            $balanceOnly=((string)($_POST['balance_only']??'')==='1') || ($queueId<1 && $invoiceId>0);
            if($balanceOnly)$queueId=0;
            $pdo->beginTransaction();
            require_patient($patientId);
            $session=revenue_lock_cashier_transaction($pdo,$branchId,$userId,$patientId,$queueId,$balanceOnly?'Balance-only account payment started by cashier':'Transaction started by cashier');
            if(schema_column_exists(table('cashier_sessions'),'active_invoice_id')||schema_column_exists(table('cashier_sessions'),'transaction_mode')){
                $sets=[];$args=[];
                if(schema_column_exists(table('cashier_sessions'),'active_invoice_id')){$sets[]='active_invoice_id=?';$args[]=$invoiceId>0?$invoiceId:null;}
                if(schema_column_exists(table('cashier_sessions'),'transaction_mode')){$sets[]='transaction_mode=?';$args[]=$balanceOnly?'balance_only':'patient_visit';}
                if(schema_column_exists(table('cashier_sessions'),'lock_started_at')){$sets[]='lock_started_at=COALESCE(lock_started_at,NOW())';}
                if($sets){$args[]=(int)$session['id'];$pdo->prepare('UPDATE `' . table('cashier_sessions') . '` SET '.implode(',',$sets).' WHERE id=?')->execute($args);}
            }
            audit('cashier_transaction_locked','cashier_session',(int)$session['id'],['patient_id'=>$patientId,'queue_entry_id'=>$queueId?:null,'invoice_id'=>$invoiceId?:null,'mode'=>$balanceOnly?'balance_only':'patient_visit','window_id'=>$session['window_id']??null]);
            $pdo->commit();
            $query=['patient_id'=>$patientId,'transaction'=>1];if($queueId>0)$query['queue_entry_id']=$queueId;if($invoiceId>0){$query['invoice_id']=$invoiceId;if($balanceOnly)$query['balance_only']=1;}if($encounterId>0&&!$balanceOnly)$query['encounter_id']=$encounterId;
            header('Location: billing.php?'.http_build_query($query));exit;
        }

        if ($action === 'cancel_transaction') {
            $reason=trim((string)($_POST['cancel_reason']??''));
            if(mb_strlen($reason)<5)throw new RuntimeException('Enter a clear cancellation reason.');
            $pdo->beginTransaction();
            $session=revenue_require_cashier_session($pdo,$branchId,$userId,true);
            $patientId=(int)($session['locked_patient_id']??0);
            if($patientId<1)throw new RuntimeException('There is no active cashier transaction to cancel.');
            revenue_release_cashier_transaction($pdo,(int)$session['id'],$patientId,'Cancelled: '.$reason);
            audit('cashier_transaction_cancelled','cashier_session',(int)$session['id'],['patient_id'=>$patientId,'queue_entry_id'=>$session['locked_queue_entry_id']??null,'reason'=>$reason,'window_id'=>$session['window_id']??null]);
            $pdo->commit();
            if (function_exists('dental_clear_patient_context')) dental_clear_patient_context($patientId); else unset($_SESSION['dental_patient_context']);
            flash('warning','Cashier transaction cancelled. No payment was posted.');
            header('Location: billing.php');exit;
        }

        if ($action === 'return_billing_correction') {
            if (!workflow_governance_schema_ready()) throw new RuntimeException('Apply Version 0.5.9.8.16 before returning a visit for billing correction.');
            $patientId=(int)($_POST['patient_id']??0);$queueId=(int)($_POST['queue_entry_id']??0);$reason=trim((string)($_POST['correction_reason']??''));
            if($patientId<1||$queueId<1)throw new RuntimeException('The patient and cashier visit could not be verified.');
            if(mb_strlen($reason)<10)throw new RuntimeException('Enter a specific billing-correction reason of at least 10 characters.');
            $pdo->beginTransaction();
            $session=revenue_require_cashier_session($pdo,$branchId,$userId,true);
            revenue_assert_cashier_transaction($session,$patientId,$queueId);
            $queueStmt=$pdo->prepare('SELECT * FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=? FOR UPDATE');
            $queueStmt->execute([$queueId,$patientId,$branchId]);$queue=$queueStmt->fetch();
            if(!$queue||!in_array((string)$queue['status'],['waiting_billing','cashier_called'],true))throw new RuntimeException('This visit is no longer in the cashier stage.');
            $encounterStmt=$pdo->prepare('SELECT * FROM `' . table('clinical_encounters') . '` WHERE queue_entry_id=? ORDER BY id DESC LIMIT 1 FOR UPDATE');
            $encounterStmt->execute([$queueId]);$encounter=$encounterStmt->fetch()?:null;
            if(!$encounter)throw new RuntimeException('The finalized clinical encounter could not be located for correction.');
            $invoiceStmt=$pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE queue_entry_id=? AND status NOT IN ("voided","refunded") ORDER BY id DESC LIMIT 1 FOR UPDATE');
            $invoiceStmt->execute([$queueId]);$invoice=$invoiceStmt->fetch()?:null;
            $requestIds=governance_request_billing_correction($pdo,$queue,$encounter,$invoice,$userId,$reason);
            $oldStatus=(string)$queue['status'];
            $update=$pdo->prepare('UPDATE `' . table('queue_entries') . '` SET status="billing_correction",updated_at=NOW() WHERE id=? AND status=?');
            $update->execute([$queueId,$oldStatus]);
            if($update->rowCount()!==1)throw new RuntimeException('The cashier queue changed while the correction request was being created.');
            cashier_queue_history($pdo,$queueId,$oldStatus,'billing_correction','Returned by Cashier for fee or procedure-source correction');
            revenue_release_cashier_transaction($pdo,(int)$session['id'],$patientId,'Returned for billing correction: '.$reason);
            audit('billing_correction_requested','queue_entry',$queueId,['patient_id'=>$patientId,'encounter_id'=>(int)$encounter['id'],'request_ids'=>$requestIds,'reason'=>$reason,'cashier_session_id'=>(int)$session['id'],'window_id'=>$session['window_id']??null]);
            $pdo->commit();
            if (function_exists('dental_clear_patient_context')) dental_clear_patient_context($patientId); else unset($_SESSION['dental_patient_context']);
            flash('warning','Visit returned to the responsible Dentist for billing correction. It will reappear at Cashier automatically after the correction is signed.');
            header('Location: billing.php');exit;
        }

        if ($action === 'call_cashier') {
            $queueId = (int)($_POST['queue_entry_id'] ?? 0);
            $pdo->beginTransaction();
            $stmt = $pdo->prepare(
                'SELECT q.status,q.patient_id,p.first_name,p.last_name FROM `' . table('queue_entries') . '` q '
                . 'JOIN `' . table('patients') . '` p ON p.id=q.patient_id WHERE q.id=? AND q.branch_id=? FOR UPDATE'
            );
            $stmt->execute([$queueId,$branchId]);
            $queue = $stmt->fetch();
            if (!$queue || (string)$queue['status'] !== 'waiting_billing') throw new RuntimeException('This patient is no longer waiting to be called by the cashier.');
            $update = $pdo->prepare('UPDATE `' . table('queue_entries') . '` SET status="cashier_called",cashier_called_at=COALESCE(cashier_called_at,NOW()),updated_at=NOW() WHERE id=? AND status="waiting_billing"');
            $update->execute([$queueId]);
            if ($update->rowCount() !== 1) throw new RuntimeException('The cashier queue state changed before the call could be recorded. Reload and try again.');
            cashier_queue_history($pdo,$queueId,'waiting_billing','cashier_called','Patient called by cashier');
            $session=revenue_lock_cashier_transaction($pdo,$branchId,$userId,(int)$queue['patient_id'],$queueId,'Patient called to cashier');
            audit('cashier_patient_called','queue_entry',$queueId,['cashier_session_id'=>$session['id'],'window_id'=>$session['window_id']??null]);
            $pdo->commit();
            flash('success', $queue['first_name'] . ' ' . $queue['last_name'] . ' has been called to the cashier.');
            header('Location: billing.php?queue_entry_id=' . $queueId); exit;
        }

        if ($action === 'pos_checkout') {
            if (!$posReady) throw new RuntimeException('Apply Version 0.5.9.8.15 before using the governed cashier register.');
            $patientId = (int)($_POST['patient_id'] ?? 0);
            $queueId = (int)($_POST['queue_entry_id'] ?? 0);
            $encounterId = (int)($_POST['encounter_id'] ?? 0);
            $invoiceId = (int)($_POST['invoice_id'] ?? 0);
            $mode = (string)($_POST['checkout_mode'] ?? 'save');
            $balanceOnlyMode = ((string)($_POST['balance_only'] ?? '') === '1');
            $cashierTaskReleased = false;
            if ($balanceOnlyMode) {
                $queueId = 0;
                $encounterId = 0;
            }
            if ($patientId < 1) throw new RuntimeException('Select a patient before using the register.');
            if (!in_array($mode, ['save','pay'], true)) $mode = 'save';
            if ($balanceOnlyMode && $invoiceId < 1) throw new RuntimeException('Select an existing payable invoice for balance-only payment.');
            if ($balanceOnlyMode && $mode === 'save') throw new RuntimeException('Balance-only mode does not edit or resave charges. Enter the payment amount and choose Receive balance payment.');

            $pdo->beginTransaction();
            $patientRecord = require_patient($patientId);
            if ((int)($patientRecord['branch_id'] ?? 0) > 0 && (int)$patientRecord['branch_id'] !== $branchId) {
                throw new RuntimeException('The patient belongs to a different clinic branch.');
            }
            $session = revenue_require_cashier_session($pdo,$branchId,$userId,true);
            revenue_assert_cashier_transaction($session,$patientId,$queueId);
            $queue = null;
            if ($queueId > 0) {
                $stmt = $pdo->prepare('SELECT * FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=? FOR UPDATE');
                $stmt->execute([$queueId,$patientId,$branchId]);
                $queue = $stmt->fetch();
                if (!$queue) throw new RuntimeException('The selected cashier queue entry does not belong to this patient.');
                if (!in_array((string)$queue['status'], ['waiting_billing','cashier_called'], true)) throw new RuntimeException('This patient is not currently in the cashier stage.');
                $encounterStmt = $pdo->prepare('SELECT id,status,patient_id,branch_id,queue_entry_id FROM `' . table('clinical_encounters') . '` WHERE queue_entry_id=? ORDER BY id DESC LIMIT 1 FOR UPDATE');
                $encounterStmt->execute([$queueId]);
                $queueEncounter = $encounterStmt->fetch() ?: null;
                if ($queueEncounter) {
                    if ($encounterId > 0 && $encounterId !== (int)$queueEncounter['id']) throw new RuntimeException('The posted encounter does not match this queue visit.');
                    $encounterId = (int)$queueEncounter['id'];
                }
            }
            if ($encounterId > 0) {
                $encounterStmt = $pdo->prepare('SELECT id,status,patient_id,branch_id,queue_entry_id FROM `' . table('clinical_encounters') . '` WHERE id=? FOR UPDATE');
                $encounterStmt->execute([$encounterId]);
                $encounter = $encounterStmt->fetch();
                if (!$encounter || (int)$encounter['patient_id'] !== $patientId || ((int)($encounter['branch_id'] ?? 0) > 0 && (int)$encounter['branch_id'] !== $branchId)) throw new RuntimeException('The clinical encounter does not belong to this patient and branch.');
                if (!in_array((string)$encounter['status'], ['finalized','addendum'], true)) throw new RuntimeException('Clinical charges can be created only from a finalized encounter.');
                if ($queueId > 0 && !empty($encounter['queue_entry_id']) && (int)$encounter['queue_entry_id'] !== $queueId) throw new RuntimeException('The clinical encounter belongs to another queue visit.');
            }

            $invoice = null;
            if ($invoiceId > 0) {
                $stmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE id=? AND patient_id=? AND branch_id=? FOR UPDATE');
                $stmt->execute([$invoiceId,$patientId,$branchId]);
                $invoice = $stmt->fetch();
                if (!$invoice || in_array((string)$invoice['status'], ['voided','refunded','written_off'], true)) throw new RuntimeException('This invoice cannot be processed.');
            }
            if ($balanceOnlyMode && $invoice) {
                $queueId = 0;
                if (!in_array((string)$invoice['status'], ['unpaid','partial'], true)) throw new RuntimeException('Balance-only payment can be accepted only for unpaid or partial invoices.');
                if (max(0, (float)$invoice['amount'] - (float)$invoice['paid_amount']) <= 0.001) throw new RuntimeException('This payable has no remaining balance.');
            }
            if ($invoice && !$balanceOnlyMode && $queueId > 0 && !empty($invoice['queue_entry_id']) && (int)$invoice['queue_entry_id'] !== $queueId) {
                throw new RuntimeException('This invoice belongs to a different queue visit and cannot be reassigned.');
            }

            $paidAlready = round((float)($invoice['paid_amount'] ?? 0), 2);
            $chargeLocked = $invoice && (!empty($invoice['charge_locked_at']) || $paidAlready > 0.001);
            $discountAuthorization = null;
            $discountAuthorizationId = (int)($_POST['discount_authorization_id'] ?? 0);
            if ($discountAuthorizationId > 0) {
                $discountAuthorization = revenue_load_discount_authorization($pdo,$discountAuthorizationId,$branchId,$patientId,$queueId ?: null,true);
            }

            if (!$chargeLocked && !$balanceOnlyMode) {
                $items = cashier_posted_items($pdo,$patientId,$branchId,$encounterId,$invoiceId,$isSupervisor);
                [$subtotal,$discountAmount,$total,$discountType,$discountValue,$discountReason] = cashier_totals($items,$discountAuthorization);
                $containsRetailProduct=count(array_filter($items,static fn(array $item):bool=>(string)($item['source_type']??'')==='retail_product'))>0;
                if($containsRetailProduct&&$mode==='save')throw new RuntimeException('Retail products cannot be saved as an unpaid invoice. Collect the full payment now so stock is deducted only after payment succeeds.');
                $authorizedNoChargeInvoice=$items&&count(array_filter($items,static fn(array $item):bool=>(string)($item['fee_status']??'')==='no_charge'))===count($items);
                if ($total <= 0.001 && !$authorizedNoChargeInvoice) throw new RuntimeException('A fully waived billable visit must use the supervised charge-waiver workflow. Only procedures explicitly authorized as no-charge may create a zero-total clinical invoice.');
                $summary = implode(' · ', array_slice(array_column($items, 'description'), 0, 2));
                if (count($items) > 2) $summary .= ' +' . (count($items) - 2) . ' more';
                if ($invoice === null) {
                    $invoiceNo = cashier_invoice_number($pdo);
                    $pdo->prepare(
                        'INSERT INTO `' . table('invoices') . '` '
                        . '(branch_id,invoice_no,patient_id,encounter_id,queue_entry_id,description,subtotal,discount_type,discount_value,discount_amount,discount_reason,discount_authorization_id,tax_amount,amount,status,created_by,created_at,updated_at) '
                        . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?, ?,"unpaid",?,NOW(),NOW())'
                    )->execute([$branchId,$invoiceNo,$patientId,$encounterId ?: null,$queueId ?: null,$summary,$subtotal,$discountType,$discountValue,$discountAmount,$discountReason,$discountAuthorizationId ?: null,0,$total,$userId ?: null]);
                    $invoiceId = (int)$pdo->lastInsertId();
                } else {
                    $invoiceUpdate = $pdo->prepare(
                        'UPDATE `' . table('invoices') . '` SET encounter_id=?,queue_entry_id=?,description=?,subtotal=?,discount_type=?,discount_value=?,discount_amount=?,discount_reason=?,discount_authorization_id=?,tax_amount=0,amount=?,updated_at=NOW() WHERE id=? AND paid_amount=0 AND charge_locked_at IS NULL'
                    );
                    $invoiceUpdate->execute([$encounterId ?: ($invoice['encounter_id'] ?? null),$queueId ?: ($invoice['queue_entry_id'] ?? null),$summary,$subtotal,$discountType,$discountValue,$discountAmount,$discountReason,$discountAuthorizationId ?: null,$total,$invoiceId]);
                    if ($invoiceUpdate->rowCount() !== 1) throw new RuntimeException('The invoice was locked or changed before the charge update completed. Reload the register.');
                }
                $chargeHash = cashier_save_items($pdo,$invoiceId,$items,$discountAmount,$userId);
                cashier_confirm_usage_items($pdo,$items,$branchId,$userId);
                $chargeUpdate = $pdo->prepare('UPDATE `' . table('invoices') . '` SET charge_source_hash=?,charge_locked_at=' . ($discountAuthorization ? 'NOW()' : 'NULL') . ',charge_locked_by=? WHERE id=?');
                $chargeUpdate->execute([$chargeHash,$discountAuthorization ? $userId : null,$invoiceId]);
                if ($discountAuthorization) revenue_mark_discount_used($pdo,$discountAuthorizationId,$invoiceId,$userId);
            } elseif ($balanceOnlyMode) {
                // Existing balance payment must never alter the clinical/charge source.
                // The cashier is paying a receivable only; no checkup, queue entry, or
                // new procedure is created by this path.
                if ($discountAuthorizationId > 0) throw new RuntimeException('Discount changes are not allowed during balance-only payment. Open supervisor review if the account balance is wrong.');
            } elseif ($discountAuthorizationId > 0 && (int)($invoice['discount_authorization_id'] ?? 0) !== $discountAuthorizationId) {
                throw new RuntimeException('A locked invoice cannot accept a different discount authorization.');
            }

            $invoiceStmt = $pdo->prepare('SELECT i.*,q.appointment_id FROM `' . table('invoices') . '` i LEFT JOIN `' . table('queue_entries') . '` q ON q.id=i.queue_entry_id WHERE i.id=? FOR UPDATE');
            $invoiceStmt->execute([$invoiceId]);
            $invoice = $invoiceStmt->fetch();
            if (!$invoice) throw new RuntimeException('Invoice could not be loaded after saving.');
            $invoiceHasRetailProducts=cashier_invoice_has_retail_products($pdo,$invoiceId);
            if($invoiceHasRetailProducts&&$mode==='save')throw new RuntimeException('Retail products require full payment in the same cashier transaction. They cannot remain on an unpaid invoice.');
            if ($mode === 'pay' || !empty($invoice['charge_locked_at']) || (float)$invoice['paid_amount'] > 0.001) {
                revenue_assert_invoice_integrity($pdo, $invoice);
            }
            $effectiveQueueId = (int)($invoice['queue_entry_id'] ?? 0);
            if ($mode === 'pay' && $effectiveQueueId > 0) {
                if (!$queue || (int)$queue['id'] !== $effectiveQueueId) {
                    $queueStmt = $pdo->prepare('SELECT * FROM `' . table('queue_entries') . '` WHERE id=? AND patient_id=? AND branch_id=? FOR UPDATE');
                    $queueStmt->execute([$effectiveQueueId,$patientId,$branchId]);
                    $queue = $queueStmt->fetch() ?: null;
                }
                if (!$queue) throw new RuntimeException('The queue visit linked to this invoice could not be verified.');
                $linkedQueueStatus = (string)($queue['status'] ?? '');
                if ($linkedQueueStatus === 'waiting_billing') throw new RuntimeException('Call the patient at the cashier before taking payment for the current visit. You may save the invoice while the patient is still waiting.');
                // Historical or already-completed queue visits remain collectible as
                // account receivables without reopening or altering the old queue ticket.
            }

            $balance = max(0, round((float)$invoice['amount'] - (float)$invoice['paid_amount'], 2));
            $paymentId = 0; $change = 0.0; $paymentApplied = 0.0; $allocationSnapshotStored = true;
            if ($mode === 'pay') {
                if ($balance <= 0.001) throw new RuntimeException('This invoice is already fully paid.');
                $paymentRequestToken=trim((string)($_POST['payment_request_token']??''));
                if(!preg_match('/^[a-f0-9]{64}$/',$paymentRequestToken))throw new RuntimeException('The payment request token is invalid. Reload the Cashier Register and try again.');
                if(schema_column_exists(table('payments'),'request_token')){
                    $duplicatePayment=$pdo->prepare('SELECT id FROM `' . table('payments') . '` WHERE request_token=? LIMIT 1 FOR UPDATE');
                    $duplicatePayment->execute([$paymentRequestToken]);
                    if((int)($duplicatePayment->fetchColumn()?:0)>0)throw new RuntimeException('This payment request was already processed. Reload the register instead of submitting it again.');
                }
                $method = (string)($_POST['method'] ?? 'cash');
                $methodRule = revenue_payment_method_rule($pdo, $method);
                if (empty($methodRule['is_active'])) throw new RuntimeException('Select an active payment method.');
                $method = (string)$methodRule['method_code'];
                $referenceRequired = revenue_payment_method_requires_reference($pdo, $method);
                $methodAllowsChange = revenue_payment_method_allows_change($pdo, $method);
                $tendered = round((float)($_POST['tendered_amount'] ?? 0), 2);
                if ($tendered <= 0) throw new RuntimeException('Enter the amount tendered or paid.');
                if($invoiceHasRetailProducts&&$tendered+0.001<$balance)throw new RuntimeException('Retail products require full payment. Enter at least the complete remaining balance so stock can be deducted after the payment posts.');
                $reference = revenue_clean_payment_reference((string)($_POST['reference_no'] ?? ''));
                if ($referenceRequired && mb_strlen($reference) < 4) throw new RuntimeException('Enter the required external card, GCash, Maya, bank, cheque, mixed-payment, or digital transaction reference code.');
                if (!$methodAllowsChange && $tendered > $balance + 0.001) throw new RuntimeException('This payment method cannot exceed the remaining invoice balance.');
                if ($referenceRequired) {
                    $paymentReferenceLock = revenue_lock_name('payment_reference', $branchId, $method, mb_strtolower($reference));
                    revenue_acquire_lock($pdo, $paymentReferenceLock);
                    $duplicateReference = $pdo->prepare(
                        'SELECT COUNT(*) FROM `' . table('payments') . '` pay JOIN `' . table('invoices') . '` inv ON inv.id=pay.invoice_id '
                        . 'WHERE inv.branch_id=? AND pay.method=? AND pay.reference_no=?'
                    );
                    $duplicateReference->execute([$branchId,$method,$reference]);
                    if ((int)$duplicateReference->fetchColumn() > 0) throw new RuntimeException('This payment reference has already been posted in this branch.');
                }
                $payerName = trim((string)($_POST['payer_name'] ?? ''));
                $payerRelationship = trim((string)($_POST['payer_relationship'] ?? 'Self'));
                if ($payerName === '') $payerName = trim((string)$patientRecord['first_name'] . ' ' . (string)$patientRecord['last_name']);
                if (mb_strlen($payerName) < 2 || mb_strlen($payerName) > 190) throw new RuntimeException('Enter the name of the patient or payee providing the payment.');
                $allowedPayerRelationships = ['Self','Parent','Guardian','Spouse','Relative','Employer','Other'];
                if (!in_array($payerRelationship,$allowedPayerRelationships,true)) $payerRelationship = 'Other';

                $paymentApplied = min($balance, $tendered);
                $change = $methodAllowsChange ? max(0, round($tendered - $paymentApplied, 2)) : 0.0;
                $paymentNotes = trim((string)($_POST['payment_notes'] ?? '')) ?: null;
                if (schema_column_exists(table('payments'),'payer_name') && schema_column_exists(table('payments'),'payer_relationship')) {
                    $pdo->prepare(
                        'INSERT INTO `' . table('payments') . '` (invoice_id,cashier_session_id,cashier_window_id,source_type,status,amount,adjustment_total,tendered_amount,change_amount,method,reference_no,payer_name,payer_relationship,notes,received_by,created_at) '
                        . 'VALUES (?, ?, ?, "patient", "posted", ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, NOW())'
                    )->execute([$invoiceId,(int)$session['id'],(int)$session['window_id'],$paymentApplied,$tendered,$change,$method,$reference ?: null,$payerName,$payerRelationship,$paymentNotes,$userId ?: null]);
                } else {
                    $legacyNote = trim('Paid by ' . $payerName . ' (' . $payerRelationship . ')' . ($paymentNotes ? ' · ' . $paymentNotes : ''));
                    $pdo->prepare(
                        'INSERT INTO `' . table('payments') . '` (invoice_id,cashier_session_id,cashier_window_id,source_type,status,amount,adjustment_total,tendered_amount,change_amount,method,reference_no,notes,received_by,created_at) '
                        . 'VALUES (?, ?, ?, "patient", "posted", ?, 0, ?, ?, ?, ?, ?, ?, ?, NOW())'
                    )->execute([$invoiceId,(int)$session['id'],(int)$session['window_id'],$paymentApplied,$tendered,$change,$method,$reference ?: null,$legacyNote,$userId ?: null]);
                }
                $paymentId = (int)$pdo->lastInsertId();
                if(schema_column_exists(table('payments'),'request_token')){
                    $tokenUpdate=$pdo->prepare('UPDATE `' . table('payments') . '` SET request_token=? WHERE id=? AND request_token IS NULL');
                    $tokenUpdate->execute([$paymentRequestToken,$paymentId]);
                    if($tokenUpdate->rowCount()!==1)throw new RuntimeException('The payment request could not be protected against duplicate submission. No payment was posted.');
                }
                if (schema_column_exists(table('payments'), 'reference_required')) {
                    $pdo->prepare('UPDATE `' . table('payments') . '` SET reference_required=? WHERE id=?')->execute([$referenceRequired ? 1 : 0, $paymentId]);
                }
                $allocationSnapshotStored = revenue_store_payment_item_allocations($pdo,$paymentId,$invoiceId);
                $newPaid = round((float)$invoice['paid_amount'] + $paymentApplied, 2);
                $newStatus = $newPaid + 0.001 >= (float)$invoice['amount'] ? 'paid' : 'partial';
                revenue_record_transaction_event($pdo, 'payment_posted', 'Payment posted at cashier window', [
                    'branch_id'=>$branchId,
                    'patient_id'=>(int)$invoice['patient_id'],
                    'invoice_id'=>$invoiceId,
                    'payment_id'=>$paymentId,
                    'cashier_session_id'=>(int)$session['id'],
                    'cashier_window_id'=>(int)($session['window_id'] ?? 0),
                    'amount'=>$paymentApplied,
                    'previous_status'=>(string)$invoice['status'],
                    'new_status'=>$newStatus,
                    'metadata'=>['method'=>$method,'reference_required'=>$referenceRequired,'reference_masked'=>$reference !== '' ? substr($reference, 0, 4) . '…' : ''],
                ]);
                $paymentUpdate = $pdo->prepare(
                    'UPDATE `' . table('invoices') . '` SET paid_amount=?,status=?,charge_locked_at=COALESCE(charge_locked_at,NOW()),charge_locked_by=COALESCE(charge_locked_by,?),pos_finalized_at=IF(?="paid",NOW(),pos_finalized_at),pos_finalized_by=IF(?="paid",?,pos_finalized_by),updated_at=NOW() WHERE id=? AND paid_amount=?'
                );
                $paymentUpdate->execute([$newPaid,$newStatus,$userId,$newStatus,$newStatus,$userId,$invoiceId,(float)$invoice['paid_amount']]);
                if ($paymentUpdate->rowCount() !== 1) throw new RuntimeException('The invoice payment balance changed before posting. Reload the register and verify the latest balance.');
                if ($newStatus === 'paid') {
                    if($invoiceHasRetailProducts)cashier_fulfill_retail_products($pdo,$invoiceId,$branchId,(string)$invoice['invoice_no']);
                    $completedQueueId=$balanceOnlyMode?0:(int)($invoice['queue_entry_id']??0);
                    if($completedQueueId>0 && $queue && (string)($queue['status']??'')==='cashier_called') cashier_complete_visit($pdo,$completedQueueId,(int)$invoice['patient_id'],!empty($invoice['appointment_id'])?(int)$invoice['appointment_id']:null,'Paid in full at '.(string)($session['window_name']??'cashier window').'.');
                    revenue_record_checkout($pdo,$session,(int)$invoice['patient_id'],$completedQueueId,!empty($invoice['appointment_id'])&&!$balanceOnlyMode?(int)$invoice['appointment_id']:null,$invoiceId,$balanceOnlyMode?'balance_payment':'paid',$paymentApplied,$balanceOnlyMode?'Balance-only payment completed; no checkup or queue visit created.':'Payment completed',null);
                    revenue_release_cashier_transaction($pdo,(int)$session['id'],(int)$invoice['patient_id'],$balanceOnlyMode?'Completed balance-only payment':'Completed paid transaction');
                    $cashierTaskReleased = true;
                } elseif ($balanceOnlyMode) {
                    revenue_record_checkout($pdo,$session,(int)$invoice['patient_id'],0,null,$invoiceId,'partial_balance',$paymentApplied,'Partial balance-only payment recorded; remaining receivable stays on account.',null);
                    revenue_release_cashier_transaction($pdo,(int)$session['id'],(int)$invoice['patient_id'],'Partial balance-only payment recorded');
                    $cashierTaskReleased = true;
                }
                workflow_event((int)$invoice['patient_id'],$balanceOnlyMode?($newStatus === 'paid'?'balance_payment_completed':'balance_payment_partial'):($newStatus === 'paid' ? 'payment_completed' : 'payment_partial'),$balanceOnlyMode?($newStatus === 'paid'?'Balance payment completed':'Partial balance payment recorded'):($newStatus === 'paid' ? 'Payment completed' : 'Partial payment recorded'),['appointment_id'=>$balanceOnlyMode?0:(int)($invoice['appointment_id'] ?? 0),'queue_entry_id'=>$balanceOnlyMode?0:(int)($invoice['queue_entry_id'] ?? 0),'invoice_id'=>$invoiceId,'balance_only'=>$balanceOnlyMode],'₱' . number_format($paymentApplied,2) . ' · ' . $method . ($change > 0 ? ' · Change ₱' . number_format($change,2) : ''));
                audit_required('pos_payment_recorded','invoice',$invoiceId,['payment_id'=>$paymentId,'session_id'=>$session['id'],'window_id'=>$session['window_id']??null,'window_name'=>$session['window_name']??null,'amount'=>$paymentApplied,'tendered'=>$tendered,'change'=>$change,'method'=>$method,'status'=>$newStatus,'payer_name'=>$payerName,'payer_relationship'=>$payerRelationship,'balance_only'=>$balanceOnlyMode,'item_allocation_snapshot'=>$allocationSnapshotStored?'stored':'derived_fallback']);
            } else {
                audit_required('pos_invoice_saved','invoice',$invoiceId,['patient_id'=>$patientId,'queue_entry_id'=>$queueId ?: null,'amount'=>(float)$invoice['amount'],'charge_source_hash'=>$invoice['charge_source_hash'] ?? null]);
            }
            $pdo->commit();
            if ($paymentReferenceLock !== null) {
                revenue_release_lock($pdo, $paymentReferenceLock);
                $paymentReferenceLock = null;
            }

            if ($paymentId > 0) {
                flash('success',($balanceOnlyMode?'Balance payment recorded: ':'Payment recorded: ') . '₱' . number_format($paymentApplied,2) . ($change > 0 ? ' · Change due ₱' . number_format($change,2) : '') . '.');
                if (!$allocationSnapshotStored) flash('warning','The payment is valid and posted. Its itemized receipt will use the safe derived allocation until System Setup repairs the receipt snapshot table.');
                try { $delivery = cashier_send_payment_receipt($pdo,$paymentId); } catch (Throwable $mailException) { $delivery = ['status'=>'failed','error'=>$mailException->getMessage()]; }
                $feedback = email_delivery_feedback($delivery,'Payment receipt'); flash($feedback['type'],$feedback['message']);
            } else {
                flash('success','Invoice saved with server-verified charge sources.');
            }
            if ($cashierTaskReleased) {
                if (function_exists('dental_clear_patient_context')) dental_clear_patient_context($patientId); else unset($_SESSION['dental_patient_context']);
                header('Location: billing.php' . ($paymentId > 0 ? '?completed_payment_id=' . $paymentId : '')); exit;
            }
            header('Location: billing.php?patient_id=' . $patientId . '&invoice_id=' . $invoiceId . ($queueId > 0 ? '&queue_entry_id=' . $queueId : '') . ($paymentId > 0 ? '&payment_id=' . $paymentId : '') . '&transaction=1'); exit;
        }

        if ($action === 'resend_receipt') {
            $paymentId = (int)($_POST['payment_id'] ?? 0);
            if ($paymentId < 1) throw new RuntimeException('Select a payment receipt to resend.');
            $receiptScope = $pdo->prepare('SELECT i.patient_id FROM `' . table('payments') . '` p JOIN `' . table('invoices') . '` i ON i.id=p.invoice_id WHERE p.id=? AND i.branch_id=?');
            $receiptScope->execute([$paymentId,$branchId]);
            $receiptPatientId = (int)($receiptScope->fetchColumn() ?: 0);
            if ($receiptPatientId < 1) throw new RuntimeException('The payment receipt is outside this branch.');
            require_patient($receiptPatientId);
            try { $delivery = cashier_send_payment_receipt($pdo,$paymentId); } catch (Throwable $mailException) { $delivery = ['status'=>'failed','error'=>$mailException->getMessage()]; }
            $feedback = email_delivery_feedback($delivery,'Payment receipt');
            audit('payment_receipt_resent','payment',$paymentId,['status'=>$delivery['status'] ?? 'unknown','outbox_id'=>$delivery['id'] ?? null]);
            flash($feedback['type'],$feedback['message']);
            header('Location: billing.php'); exit;
        }

        if ($action === 'complete_cashier') {
            $queueId = (int)($_POST['queue_entry_id'] ?? 0);
            $zeroAuthorizationId = (int)($_POST['zero_charge_authorization_id'] ?? 0);
            $zeroBalanceReason=trim((string)($_POST['zero_balance_reason']??''));
            $pdo->beginTransaction();
            $stmt = $pdo->prepare('SELECT patient_id,appointment_id,status FROM `' . table('queue_entries') . '` WHERE id=? AND branch_id=? FOR UPDATE');
            $stmt->execute([$queueId,$branchId]);
            $queue = $stmt->fetch();
            if (!$queue) throw new RuntimeException('Cashier queue entry not found.');
            if ((string)$queue['status'] !== 'cashier_called') throw new RuntimeException('Call the patient at the cashier before completing any paid, insured, or zero-balance visit.');
            $session=revenue_require_cashier_session($pdo,$branchId,$userId,true);
            revenue_assert_cashier_transaction($session,(int)$queue['patient_id'],$queueId);

            // A missing clinical fee is never equivalent to an authorized free visit.
            // Recheck the finalized encounter on the server so a crafted POST cannot
            // bypass the Cashier Register warning and close an unpriced procedure.
            if (schema_table_exists(table('clinical_encounters'))) {
                $encounterStmt = $pdo->prepare(
                    'SELECT id FROM `' . table('clinical_encounters') . '` '
                    . 'WHERE queue_entry_id=? AND patient_id=? ORDER BY id DESC LIMIT 1'
                );
                $encounterStmt->execute([$queueId,(int)$queue['patient_id']]);
                $completionEncounterId = (int)($encounterStmt->fetchColumn() ?: 0);
                if ($completionEncounterId > 0) {
                    $completionPreview = cashier_encounter_charge_preview($pdo,$completionEncounterId);
                    if ((int)($completionPreview['missing_fee_count'] ?? 0) > 0) {
                        throw new RuntimeException('Checkout is blocked because a completed procedure has no approved billing source. Return the encounter to the Dentist or Administrator for service and fee correction.');
                    }
                }
            }
            $stats = $pdo->prepare(
                'SELECT COUNT(*) invoice_count,COALESCE(SUM(amount),0) total_charges,COALESCE(SUM(CASE WHEN status IN ("unpaid","partial") THEN amount-paid_amount ELSE 0 END),0) balance '
                . 'FROM `' . table('invoices') . '` WHERE queue_entry_id=? AND status NOT IN ("voided","refunded")'
            );
            $stats->execute([$queueId]);
            $totals = $stats->fetch();
            $insuranceReceivable = 0.0;
            if ((float)$totals['balance'] > 0.001) {
                $claimStmt = $pdo->prepare(
                    'SELECT COALESCE(SUM(GREATEST(c.approved_amount-c.settled_amount,0)),0) FROM `' . table('insurance_claims') . '` c '
                    . 'JOIN `' . table('invoices') . '` i ON i.id=c.invoice_id WHERE i.queue_entry_id=? AND i.branch_id=? '
                    . 'AND c.status IN ("approved","partially_approved")'
                );
                $claimStmt->execute([$queueId,$branchId]);
                $insuranceReceivable = round((float)$claimStmt->fetchColumn(),2);
            }
            $patientResponsibility = max(0,round((float)$totals['balance']-$insuranceReceivable,2));
            if ($patientResponsibility > 0.001) throw new RuntimeException('This patient still has a responsibility balance of ₱' . number_format($patientResponsibility,2) . ' after approved insurance receivables.');
            $completionDetail = 'All queue-linked balances cleared.';
            $checkoutType='settled';$checkoutReason=null;$authorizationId=null;
            if ((int)$totals['invoice_count'] === 0 || (float)$totals['total_charges'] <= 0.001) {
                if(mb_strlen($zeroBalanceReason)<5)throw new RuntimeException('Enter why this visit has no amount due before completing zero-balance checkout.');
                $checkoutType='zero_balance';$checkoutReason=$zeroBalanceReason;
                $completionDetail='Zero-balance checkout: '.$zeroBalanceReason;
            } elseif ((float)$totals['balance'] > 0.001) {
                $checkoutType='insurance';$checkoutReason='Approved insurer receivable';
                $completionDetail = 'Patient responsibility cleared; ₱' . number_format((float)$totals['balance'],2) . ' remains as an approved insurer receivable.';
            }
            cashier_complete_visit($pdo,$queueId,(int)$queue['patient_id'],$queue['appointment_id'] ? (int)$queue['appointment_id'] : null,$completionDetail);
            $checkoutId=revenue_record_checkout($pdo,$session,(int)$queue['patient_id'],$queueId,$queue['appointment_id']?(int)$queue['appointment_id']:null,null,$checkoutType,0.0,$checkoutReason,$authorizationId);
            revenue_release_cashier_transaction($pdo,(int)$session['id'],(int)$queue['patient_id'],'Completed '.$checkoutType.' checkout');
            audit_required('cashier_visit_completed','queue_entry',$queueId,['checkout_id'=>$checkoutId,'checkout_type'=>$checkoutType,'zero_balance_reason'=>$checkoutReason,'insurance_receivable'=>$insuranceReceivable,'patient_responsibility'=>$patientResponsibility,'cashier_session_id'=>$session['id'],'window_id'=>$session['window_id']??null]);
            $pdo->commit();
            if (function_exists('dental_clear_patient_context')) dental_clear_patient_context((int)$queue['patient_id']); else unset($_SESSION['dental_patient_context']);
            flash('success','Cashier processing completed.');
            header('Location: billing.php'); exit;
        }

        throw new RuntimeException('Unsupported billing action.');
    } catch (Throwable $exception) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        if ($paymentReferenceLock !== null) {
            revenue_release_lock($pdo, $paymentReferenceLock);
            $paymentReferenceLock = null;
        }
        $error = $exception->getMessage();
    }
}

$activeSession = $posReady ? revenue_active_cashier_session($pdo,$branchId,$userId) : null;
$cashierWindows=$posReady?revenue_cashier_windows($pdo,$branchId,true):[];
$sessionTotals = $activeSession ? revenue_cashier_session_totals($pdo,(int)$activeSession['id']) : null;
$staleOpenSessions = [];
if ($posReady && $isSupervisor) {
    try {
        $staleStmt = $pdo->prepare(
            'SELECT cs.*,u.name cashier_name,COALESCE(w.window_name,cs.window_name) resolved_window_name '
            . 'FROM `' . table('cashier_sessions') . '` cs '
            . 'LEFT JOIN `' . table('users') . '` u ON u.id=cs.cashier_user_id '
            . 'LEFT JOIN `' . table('cashier_windows') . '` w ON w.id=cs.window_id '
            . 'WHERE cs.branch_id=? AND cs.status="open" AND cs.opened_at<DATE_SUB(NOW(),INTERVAL 18 HOUR) '
            . 'ORDER BY cs.opened_at,cs.id'
        );
        $staleStmt->execute([$branchId]);
        foreach ($staleStmt->fetchAll() as $staleRow) {
            $staleRow['totals'] = revenue_cashier_session_totals($pdo, (int)$staleRow['id']);
            $staleRow['expected_cash_preview'] = round((float)$staleRow['opening_cash'] + (float)$staleRow['totals']['cash_received'] - (float)$staleRow['totals']['cash_returned'], 2);
            $staleOpenSessions[] = $staleRow;
        }
    } catch (Throwable $staleSessionError) {
        error_log('DentalCare stale cashier session list failed: ' . $staleSessionError->getMessage());
    }
}
$exceptionCounts=['drawer'=>0,'discounts'=>0,'no_charge'=>0,'returns_review'=>0,'returns_process'=>0,'billing_corrections'=>0,'legacy'=>0];
$exceptionTotal=0;$openBillingCorrectionCount=0;
if($posReady&&$isSupervisor){
    $countQueries=[
        'drawer'=>'SELECT COUNT(*) FROM `' . table('cashier_sessions') . '` WHERE branch_id=? AND status="closed_pending_review"',
        'discounts'=>'SELECT COUNT(*) FROM `' . table('discount_authorizations') . '` WHERE branch_id=? AND status="pending"',
        'no_charge'=>'SELECT COUNT(*) FROM `' . table('zero_charge_authorizations') . '` WHERE branch_id=? AND status="pending"',
        'returns_review'=>'SELECT COUNT(*) FROM `' . table('payment_adjustments') . '` WHERE branch_id=? AND status="requested"',
        'returns_process'=>'SELECT COUNT(*) FROM `' . table('payment_adjustments') . '` WHERE branch_id=? AND status="approved"',
    ];
    foreach($countQueries as $key=>$sql){$stmt=$pdo->prepare($sql);$stmt->execute([$branchId]);$exceptionCounts[$key]=(int)$stmt->fetchColumn();}
    if(workflow_governance_schema_ready()){$stmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('billing_correction_requests') . '` WHERE branch_id=? AND status IN ("open","in_review")');$stmt->execute([$branchId]);$exceptionCounts['billing_corrections']=(int)$stmt->fetchColumn();$openBillingCorrectionCount=$exceptionCounts['billing_corrections'];}
    if(schema_column_exists(table('invoices'),'charge_source_hash')){
        $legacySql='SELECT COUNT(*) FROM `' . table('invoices') . '` i WHERE i.branch_id=? AND i.status NOT IN ("voided","refunded","written_off") AND ('
            . '(i.charge_source_hash IS NULL AND i.status IN ("unpaid","partial")) OR '
            . 'NOT EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` li WHERE li.invoice_id=i.id) OR '
            . 'LOWER(TRIM(i.description)) IN ("imported outstanding balance","outstanding balance","opening balance","previous balance")'
            . ')';
        $stmt=$pdo->prepare($legacySql);$stmt->execute([$branchId]);$exceptionCounts['legacy']=(int)$stmt->fetchColumn();
    }
    $exceptionTotal=array_sum($exceptionCounts);
}

if($financeView==='exceptions'){
    $exceptionType=(string)($_GET['exception_type']??'');
    if(!array_key_exists($exceptionType,$exceptionCounts)){$exceptionType='drawer';foreach($exceptionCounts as $key=>$value){if($value>0){$exceptionType=$key;break;}}}
    $state=module_management_state('page','per_page',25);$page=$state['page'];$perPage=$state['per_page'];$offset=$state['offset'];$manageId=max(0,(int)($_GET['manage_id']??0));$q=trim((string)($_GET['q']??''));
    $exceptionRows=[];$managedException=null;$listParams=[$branchId];$whereExtra='';
    if($q!==''){$whereExtra=' AND ';}
    switch($exceptionType){
        case 'drawer':
            $sql='SELECT cs.*,u.name cashier_name,w.window_name FROM `' . table('cashier_sessions') . '` cs JOIN `' . table('users') . '` u ON u.id=cs.cashier_user_id LEFT JOIN `' . table('cashier_windows') . '` w ON w.id=cs.window_id WHERE cs.branch_id=? AND cs.status="closed_pending_review"';
            if($q!==''){$sql.=' AND (u.name LIKE ? OR w.window_name LIKE ? OR cs.close_notes LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like);}$sql.=' ORDER BY cs.closed_at LIMIT '.$perPage.' OFFSET '.$offset;$stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();
            if($manageId){$stmt=$pdo->prepare('SELECT cs.*,u.name cashier_name,w.window_name FROM `' . table('cashier_sessions') . '` cs JOIN `' . table('users') . '` u ON u.id=cs.cashier_user_id LEFT JOIN `' . table('cashier_windows') . '` w ON w.id=cs.window_id WHERE cs.branch_id=? AND cs.id=? AND cs.status="closed_pending_review" LIMIT 1');$stmt->execute([$branchId,$manageId]);$managedException=$stmt->fetch()?:null;}
            break;
        case 'discounts':
            $sql='SELECT d.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester FROM `' . table('discount_authorizations') . '` d JOIN `' . table('patients') . '` p ON p.id=d.patient_id JOIN `' . table('users') . '` u ON u.id=d.requested_by WHERE d.branch_id=? AND d.status="pending"';
            if($q!==''){$sql.=' AND (CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.patient_no LIKE ? OR u.name LIKE ? OR d.reason LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like,$like);}$sql.=' ORDER BY d.requested_at LIMIT '.$perPage.' OFFSET '.$offset;$stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();
            if($manageId){$stmt=$pdo->prepare('SELECT d.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester FROM `' . table('discount_authorizations') . '` d JOIN `' . table('patients') . '` p ON p.id=d.patient_id JOIN `' . table('users') . '` u ON u.id=d.requested_by WHERE d.branch_id=? AND d.id=? AND d.status="pending" LIMIT 1');$stmt->execute([$branchId,$manageId]);$managedException=$stmt->fetch()?:null;}
            break;
        case 'no_charge':
            $sql='SELECT z.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester,q.queue_no FROM `' . table('zero_charge_authorizations') . '` z JOIN `' . table('patients') . '` p ON p.id=z.patient_id JOIN `' . table('users') . '` u ON u.id=z.requested_by LEFT JOIN `' . table('queue_entries') . '` q ON q.id=z.queue_entry_id WHERE z.branch_id=? AND z.status="pending"';
            if($q!==''){$sql.=' AND (CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.patient_no LIKE ? OR q.queue_no LIKE ? OR z.reason LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like,$like);}$sql.=' ORDER BY z.requested_at LIMIT '.$perPage.' OFFSET '.$offset;$stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();
            if($manageId){$stmt=$pdo->prepare('SELECT z.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester,q.queue_no FROM `' . table('zero_charge_authorizations') . '` z JOIN `' . table('patients') . '` p ON p.id=z.patient_id JOIN `' . table('users') . '` u ON u.id=z.requested_by LEFT JOIN `' . table('queue_entries') . '` q ON q.id=z.queue_entry_id WHERE z.branch_id=? AND z.id=? AND z.status="pending" LIMIT 1');$stmt->execute([$branchId,$manageId]);$managedException=$stmt->fetch()?:null;}
            break;
        case 'returns_review':
        case 'returns_process':
            $targetStatus=$exceptionType==='returns_review'?'requested':'approved';
            $sql='SELECT a.*,i.invoice_no,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester FROM `' . table('payment_adjustments') . '` a JOIN `' . table('invoices') . '` i ON i.id=a.invoice_id JOIN `' . table('patients') . '` p ON p.id=i.patient_id LEFT JOIN `' . table('users') . '` u ON u.id=a.requested_by WHERE a.branch_id=? AND a.status="'.$targetStatus.'"';
            if($q!==''){$sql.=' AND (i.invoice_no LIKE ? OR CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.patient_no LIKE ? OR a.reason LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like,$like);}$sql.=' ORDER BY '.($targetStatus==='requested'?'a.requested_at':'a.approved_at').' LIMIT '.$perPage.' OFFSET '.$offset;$stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();
            if($manageId){$stmt=$pdo->prepare('SELECT a.*,i.invoice_no,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,u.name requester FROM `' . table('payment_adjustments') . '` a JOIN `' . table('invoices') . '` i ON i.id=a.invoice_id JOIN `' . table('patients') . '` p ON p.id=i.patient_id LEFT JOIN `' . table('users') . '` u ON u.id=a.requested_by WHERE a.branch_id=? AND a.id=? AND a.status=? LIMIT 1');$stmt->execute([$branchId,$manageId,$targetStatus]);$managedException=$stmt->fetch()?:null;}
            break;
        case 'billing_corrections':
            if(workflow_governance_schema_ready()){$sql='SELECT b.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,q.queue_no,requester.name requester_name,dentist.name assigned_dentist_name FROM `' . table('billing_correction_requests') . '` b JOIN `' . table('patients') . '` p ON p.id=b.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=b.queue_entry_id LEFT JOIN `' . table('users') . '` requester ON requester.id=b.requested_by LEFT JOIN `' . table('users') . '` dentist ON dentist.id=b.assigned_dentist_user_id WHERE b.branch_id=? AND b.status IN ("open","in_review")';if($q!==''){$sql.=' AND (CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.patient_no LIKE ? OR q.queue_no LIKE ? OR b.reason LIKE ? OR dentist.name LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like,$like,$like);}$sql.=' ORDER BY b.requested_at,b.id LIMIT '.$perPage.' OFFSET '.$offset;$stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();if($manageId){$stmt=$pdo->prepare('SELECT b.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,q.queue_no,requester.name requester_name,dentist.name assigned_dentist_name FROM `' . table('billing_correction_requests') . '` b JOIN `' . table('patients') . '` p ON p.id=b.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=b.queue_entry_id LEFT JOIN `' . table('users') . '` requester ON requester.id=b.requested_by LEFT JOIN `' . table('users') . '` dentist ON dentist.id=b.assigned_dentist_user_id WHERE b.branch_id=? AND b.id=? AND b.status IN ("open","in_review") LIMIT 1');$stmt->execute([$branchId,$manageId]);$managedException=$stmt->fetch()?:null;}}
            break;
        case 'legacy':
            if(schema_column_exists(table('invoices'),'charge_source_hash')){
                $legacySelect='SELECT i.id,i.invoice_no,i.patient_id,i.branch_id,i.description,i.subtotal,i.discount_amount,i.amount,i.paid_amount,i.status,i.created_at,i.charge_source_hash,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,q.queue_no,'
                    . '(SELECT COUNT(*) FROM `' . table('invoice_items') . '` li WHERE li.invoice_id=i.id) item_count,'
                    . '(SELECT COUNT(*) FROM `' . table('invoice_items') . '` li WHERE li.invoice_id=i.id AND LOWER(TRIM(li.description)) NOT IN ("imported outstanding balance","outstanding balance","opening balance","previous balance","patient invoice","invoice item")) specific_item_count '
                    . 'FROM `' . table('invoices') . '` i JOIN `' . table('patients') . '` p ON p.id=i.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=i.queue_entry_id WHERE i.branch_id=? AND i.status NOT IN ("voided","refunded","written_off") AND ('
                    . '(i.charge_source_hash IS NULL AND i.status IN ("unpaid","partial")) OR '
                    . 'NOT EXISTS (SELECT 1 FROM `' . table('invoice_items') . '` lx WHERE lx.invoice_id=i.id) OR '
                    . 'LOWER(TRIM(i.description)) IN ("imported outstanding balance","outstanding balance","opening balance","previous balance")'
                    . ')';
                $sql=$legacySelect;
                if($q!==''){$sql.=' AND (i.invoice_no LIKE ? OR CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.patient_no LIKE ? OR q.queue_no LIKE ? OR i.description LIKE ?)';$like=module_like_term($q);array_push($listParams,$like,$like,$like,$like,$like);}
                $sql.=' ORDER BY i.created_at DESC,i.id DESC LIMIT '.$perPage.' OFFSET '.$offset;
                $stmt=$pdo->prepare($sql);$stmt->execute($listParams);$exceptionRows=$stmt->fetchAll();
                if($manageId){$stmt=$pdo->prepare($legacySelect . ' AND i.id=? LIMIT 1');$stmt->execute([$branchId,$manageId]);$managedException=$stmt->fetch()?:null;}
            }
            break;
    }
    $rowsTotal=$exceptionCounts[$exceptionType]??0;
    render_header('Cashier Exceptions');
    if($error!=='')echo '<div class="alert danger"><strong>The exception action was not completed.</strong><br>'.e($error).'</div>';
    if(!$posReady)echo '<div class="alert warning"><strong>Cashier database update required.</strong> Apply all pending updates before approving or processing finance exceptions.</div>';
    $labels=['drawer'=>'Drawer variances','discounts'=>'Discount requests','no_charge'=>'No-charge requests','returns_review'=>'Returns to review','returns_process'=>'Approved returns','billing_corrections'=>'Billing corrections','legacy'=>'Legacy invoice reviews'];
    ?>
    <div class="finance-view-switch" role="navigation" aria-label="Finance workspace"><a href="billing.php">Cashier Register</a><a href="billing.php?view=exceptions" class="active">Cashier Exceptions <span><?=(int)$exceptionTotal?></span></a></div>
    <section class="page-heading"><div><span class="eyebrow">Supervisor workspace</span><h2>Cashier Exceptions</h2><p>Only one exception category is loaded at a time. Open a record to review or process it.</p></div></section>
    <div class="toolbar"><div class="tabs"><?php foreach($labels as $key=>$label):?><a class="<?=$exceptionType===$key?'active':''?>" href="billing.php?view=exceptions&amp;exception_type=<?=e($key)?>"><?=e($label)?> <span><?=(int)$exceptionCounts[$key]?></span></a><?php endforeach;?></div></div>
    <form method="get" class="management-filter-bar"><input type="hidden" name="view" value="exceptions"><input type="hidden" name="exception_type" value="<?=e($exceptionType)?>"><label>Search<input name="q" value="<?=e($q)?>" placeholder="Patient, invoice, queue, cashier or reason"></label><label>Rows<select name="per_page"><?php foreach([10,25,50,100] as $size):?><option value="<?=$size?>" <?=$perPage===$size?'selected':''?>><?=$size?></option><?php endforeach;?></select></label><button class="btn primary">Apply</button><a class="btn ghost" href="billing.php?view=exceptions&amp;exception_type=<?=e($exceptionType)?>">Reset</a></form>
    <section class="card server-list-card"><?php module_render_list_count($rowsTotal,$labels[$exceptionType]);module_render_server_pagination($rowsTotal,$page,$perPage,'page','per_page','exception-list');?><div class="table-wrap"><table><thead><tr><th>Type / requested</th><th>Subject</th><th>Details</th><th>Status</th><th>Action</th></tr></thead><tbody><?php foreach($exceptionRows as $row):?><tr><td><strong><?=e($labels[$exceptionType])?></strong><small><?=e(date('M j, Y g:i A',strtotime((string)($row['requested_at']??$row['closed_at']??$row['created_at']??'now'))))?></small></td><td><?php if($exceptionType==='drawer'):?><strong><?=e((string)$row['cashier_name'])?></strong><small><?=e((string)($row['window_name']?:'Window not recorded'))?></small><?php elseif($exceptionType==='billing_corrections'):?><strong><?=e((string)$row['patient'])?></strong><small><?=e((string)$row['patient_no'])?><?=!empty($row['queue_no'])?' · '.e((string)$row['queue_no']):''?></small><?php else:?><strong><?=e((string)($row['patient']??$row['invoice_no']??'Finance record'))?></strong><small><?=e((string)($row['patient_no']??$row['invoice_no']??''))?></small><?php endif;?></td><td><?php if($exceptionType==='drawer'):?>Expected ₱<?=number_format((float)$row['expected_cash'],2)?> · Counted ₱<?=number_format((float)$row['counted_cash'],2)?><small>Variance ₱<?=number_format((float)$row['variance'],2)?></small><?php elseif($exceptionType==='discounts'):?><?=e($row['discount_type']==='percent'?(string)$row['discount_value'].'%':'₱'.number_format((float)$row['discount_value'],2))?><small><?=e((string)$row['reason'])?></small><?php elseif($exceptionType==='no_charge'):?><?=e(module_status((string)$row['category']))?><small><?=e((string)$row['reason'])?></small><?php elseif(in_array($exceptionType,['returns_review','returns_process'],true)):?><?=e((string)$row['invoice_no'])?> · ₱<?=number_format((float)$row['amount'],2)?><small><?=e(module_status((string)$row['adjustment_type']))?> · <?=e((string)$row['reason'])?></small><?php elseif($exceptionType==='billing_corrections'):?>Assigned: <?=e((string)($row['assigned_dentist_name']?:'Unassigned'))?><small><?=e((string)$row['reason'])?></small><?php else:?>Total ₱<?=number_format((float)$row['amount'],2)?> · Paid ₱<?=number_format((float)$row['paid_amount'],2)?><small><?=e((string)($row['queue_no']?:'No queue'))?></small><?php endif;?></td><td><span class="badge"><?=e(module_status((string)($row['status']??'pending')))?></span></td><td><a class="btn ghost" href="<?=e(module_management_url(['view'=>'exceptions','exception_type'=>$exceptionType,'manage_id'=>(int)$row['id'],'page'=>$page,'per_page'=>$perPage]))?>">Manage</a></td></tr><?php endforeach;if(!$exceptionRows):?><tr><td colspan="5" class="muted">No records are waiting in this category.</td></tr><?php endif;?></tbody></table></div><?php module_render_server_pagination($rowsTotal,$page,$perPage,'page','per_page','exception-list');?></section>
    <?php if($managedException):?><dialog id="exception-manage" class="wide-dialog" data-auto-open-dialog="exception-manage"><form method="dialog"><button type="submit" class="dialog-close">×</button></form><div class="dialog-scroll"><h2><?=e($labels[$exceptionType])?></h2>
    <?php if($exceptionType==='drawer'):?><p><strong><?=e((string)$managedException['cashier_name'])?></strong> · <?=e((string)($managedException['window_name']?:'Window not recorded'))?></p><div class="detail-grid"><div><small>Expected cash</small><strong>₱<?=number_format((float)$managedException['expected_cash'],2)?></strong></div><div><small>Counted cash</small><strong>₱<?=number_format((float)$managedException['counted_cash'],2)?></strong></div><div><small>Variance</small><strong>₱<?=number_format((float)$managedException['variance'],2)?></strong></div></div><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="return_view" value="exceptions"><input type="hidden" name="action" value="review_session"><input type="hidden" name="session_id" value="<?=(int)$managedException['id']?>"><label class="wide">Review note<textarea name="review_note" required></textarea></label><button class="btn primary">Acknowledge and close</button></form>
    <?php elseif(in_array($exceptionType,['discounts','no_charge','returns_review'],true)):?><p><strong><?=e((string)$managedException['patient'])?></strong></p><div class="notice info"><?=e((string)$managedException['reason'])?></div><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="return_view" value="exceptions"><input type="hidden" name="action" value="<?=$exceptionType==='discounts'?'review_discount':($exceptionType==='no_charge'?'review_zero_charge':'review_adjustment')?>"><?php if($exceptionType==='returns_review'):?><input type="hidden" name="adjustment_id" value="<?=(int)$managedException['id']?>"><?php else:?><input type="hidden" name="authorization_id" value="<?=(int)$managedException['id']?>"><?php endif;?><label class="wide">Review note<textarea name="review_note"></textarea></label><div class="status-actions wide"><button class="btn primary" name="decision" value="approved">Approve</button><button class="btn ghost" name="decision" value="rejected">Reject</button></div></form>
    <?php elseif($exceptionType==='returns_process'):?><p><strong><?=e((string)$managedException['invoice_no'])?> · <?=e((string)$managedException['patient'])?></strong></p><p><?=e(module_status((string)$managedException['adjustment_type']))?> · ₱<?=number_format((float)$managedException['amount'],2)?> · <?=e(module_status((string)$managedException['method']))?></p><?php if(!$activeSession):?><div class="alert warning">Open your cashier window before processing this return.</div><?php endif;?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="return_view" value="exceptions"><input type="hidden" name="action" value="process_adjustment"><input type="hidden" name="adjustment_id" value="<?=(int)$managedException['id']?>"><button class="btn primary" <?=$activeSession?'':'disabled'?>>Process return</button></form>
    <?php elseif($exceptionType==='billing_corrections'):?><p><strong><?=e((string)$managedException['patient'])?></strong> · <?=e((string)($managedException['queue_no']?:'No queue'))?></p><div class="notice warning"><?=e((string)$managedException['reason'])?></div><p>Responsible Dentist: <?=e((string)($managedException['assigned_dentist_name']?:'Unassigned'))?></p><?php if((int)$managedException['encounter_id']>0):?><a class="btn primary" href="clinical_workspace.php?encounter_id=<?=(int)$managedException['encounter_id']?>">Open encounter</a><?php endif;?>
    <?php else:
        $legacyNeedsBreakdown=(int)($managedException['item_count']??0)===0
            || (int)($managedException['specific_item_count']??0)===0
            || revenue_receipt_description_is_generic((string)($managedException['description']??''));
    ?><p><strong><?=e((string)$managedException['invoice_no'])?> · <?=e((string)$managedException['patient'])?></strong></p><p>Total ₱<?=number_format((float)$managedException['amount'],2)?> · Paid ₱<?=number_format((float)$managedException['paid_amount'],2)?> · <?=e(module_status((string)$managedException['status']))?></p>
    <?php if($legacyNeedsBreakdown):?><div class="notice warning"><strong>This invoice has no true service breakdown.</strong> “<?=e((string)($managedException['description']?:'Imported balance'))?>” is only an opening-balance label. Reconstruct it from the old chart, receipt, or ledger before presenting it as an itemized patient receipt.</div>
    <form method="post" class="form-grid legacy-breakdown-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="return_view" value="exceptions"><input type="hidden" name="action" value="itemize_legacy_invoice"><input type="hidden" name="invoice_id" value="<?=(int)$managedException['id']?>"><div class="notice info wide"><strong>Total lock:</strong> The reconstructed lines must equal exactly ₱<?=number_format((float)$managedException['amount'],2)?>. Do not use sample items unless they came from the old chart, OR, or ledger.</div><label class="wide">Historical service breakdown<textarea name="item_breakdown" rows="8" required placeholder="<?=e(cashier_legacy_breakdown_placeholder((float)$managedException['amount']))?>"></textarea><small>One line per item: Description | Quantity | Historical unit price. Example must be replaced with actual historical evidence. The total must equal ₱<?=number_format((float)$managedException['amount'],2)?> exactly.</small></label><label class="wide">Reconstruction reason<textarea name="review_reason" minlength="15" required placeholder="Explain why the imported opening balance is being reconstructed and who verified the old record."></textarea></label><label class="wide">Source-document reference<input name="evidence_reference" required placeholder="Old OR no., chart page, ledger batch, migration file, or approval reference"></label><button class="btn primary">Save breakdown and rebuild receipts</button></form>
    <?php else:?><div class="notice info">This invoice already has specific line items. Review the source evidence before allowing further collection.</div><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="return_view" value="exceptions"><input type="hidden" name="action" value="reauthorize_legacy_invoice"><input type="hidden" name="invoice_id" value="<?=(int)$managedException['id']?>"><label class="wide">Review reason<textarea name="review_reason" minlength="15" required></textarea></label><label class="wide">Evidence reference<input name="evidence_reference" required></label><button class="btn primary">Verify and fingerprint invoice</button></form><?php endif;?><?php endif;?></div></dialog><?php endif;?>
    <?php render_footer();exit;
}

$queueOrderColumn = schema_column_exists(table('queue_entries'), 'billing_at') ? 'q.billing_at' : 'q.checked_in_at';
$encounterQueueLookup = cashier_schema_has_columns(table('clinical_encounters'), ['id','queue_entry_id'])
    ? '(SELECT c.id FROM `' . table('clinical_encounters') . '` c WHERE c.queue_entry_id=q.id ORDER BY c.id DESC LIMIT 1)'
    : 'NULL';
$cashierRows = [];
try {
    $cashierStmt = $pdo->prepare(
        'SELECT q.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,p.mobile,p.email,'
        . 'COALESCE(inv.invoice_count,0) invoice_count,COALESCE(inv.total_amount,0) total_amount,COALESCE(inv.total_paid,0) total_paid,COALESCE(inv.balance,0) balance,inv.latest_invoice_id,'
        . $encounterQueueLookup . ' encounter_id '
        . 'FROM `' . table('queue_entries') . '` q JOIN `' . table('patients') . '` p ON p.id=q.patient_id '
        . 'LEFT JOIN (SELECT queue_entry_id,COUNT(*) invoice_count,SUM(amount) total_amount,SUM(paid_amount) total_paid,SUM(CASE WHEN status IN ("unpaid","partial") THEN amount-paid_amount ELSE 0 END) balance,MAX(id) latest_invoice_id FROM `' . table('invoices') . '` WHERE queue_entry_id IS NOT NULL AND status NOT IN ("voided","refunded") GROUP BY queue_entry_id) inv ON inv.queue_entry_id=q.id '
        . 'WHERE q.branch_id=? AND q.status IN ("waiting_billing","cashier_called") ORDER BY (q.checked_in_at<CURDATE()) DESC,q.status="cashier_called" DESC,' . $queueOrderColumn . ',q.checked_in_at'
    );
    $cashierStmt->execute([$branchId]);
    $cashierRows = $cashierStmt->fetchAll();
} catch (Throwable $queueLoadError) {
    error_log('Cashier queue compatibility fallback: ' . $queueLoadError->getMessage());
    $cashierSchemaIssues[] = 'cashier queue query requires database repair';
}
foreach ($cashierRows as &$cashierRow) {
    $cashierRow['charge_preview'] = cashier_encounter_charge_preview($pdo,(int)($cashierRow['encounter_id'] ?? 0));
}
unset($cashierRow);

$requestedQueueId = (int)($_GET['queue_entry_id'] ?? 0);
$requestedPatientId = (int)($_GET['patient_id'] ?? 0);
$requestedInvoiceId = (int)($_GET['invoice_id'] ?? 0);
$requestedEncounterId = (int)($_GET['encounter_id'] ?? 0);
$requestedPaymentId = (int)($_GET['payment_id'] ?? 0);
$requestedBalanceOnly = (int)($_GET['balance_only'] ?? 0) === 1;
if ($requestedBalanceOnly) {
    // Balance-only payments are account payments. They must not reopen or reuse
    // an old clinical queue/checkup just because the invoice originally came
    // from a visit. The old visit remains historical; only the receivable is paid.
    $requestedQueueId = 0;
}
$lockedPatientId=(int)($activeSession['locked_patient_id']??0);
$lockedQueueId=(int)($activeSession['locked_queue_entry_id']??0);
if($lockedPatientId>0){
    if($requestedPatientId>0&&$requestedPatientId!==$lockedPatientId){$requestedInvoiceId=0;$requestedEncounterId=0;$requestedPaymentId=0;}
    $requestedPatientId=$lockedPatientId;
    if($lockedQueueId>0)$requestedQueueId=$lockedQueueId;
}
$selectedQueue = null;
foreach ($cashierRows as $row) if ((int)$row['id'] === $requestedQueueId) { $selectedQueue = $row; break; }
if (!$selectedQueue && $requestedQueueId === 0 && $requestedPatientId === 0 && $requestedInvoiceId === 0 && $requestedEncounterId === 0) foreach ($cashierRows as $row) if ($row['status'] === 'cashier_called') { $selectedQueue = $row; break; }
if (!$selectedQueue && $requestedQueueId === 0 && $requestedPatientId === 0 && $requestedInvoiceId === 0 && $requestedEncounterId === 0) $selectedQueue = $cashierRows[0] ?? null;
$selectedPatientId = $selectedQueue ? (int)$selectedQueue['patient_id'] : $requestedPatientId;
$selectedPatient = null;
if ($selectedPatientId > 0) {
    $selectedPatient = require_patient($selectedPatientId);
}
$selectedPendingFollowup = ($selectedQueue && $selectedPatientId > 0) ? cashier_pending_followup_for_queue($pdo,(int)$selectedQueue['id'],$selectedPatientId) : null;
$selectedPlannedServices = [];
if ($selectedQueue && !empty($selectedQueue['appointment_id'])) {
    $plannedMap = appointment_services_for_ids($pdo,[(int)$selectedQueue['appointment_id']]);
    $selectedPlannedServices = array_values(array_filter(
        $plannedMap[(int)$selectedQueue['appointment_id']] ?? [],
        static fn(array $segment): bool => in_array((string)($segment['status'] ?? 'planned'),['planned','completed'],true)
    ));
}

$selectedInvoice = null;
$selectedItems = [];
$selectedPatientInvoices = [];
$selectedEncounterCandidates = [];
$selectedEncounterId = $selectedQueue ? (int)($selectedQueue['encounter_id'] ?? 0) : 0;
if ($selectedPatient) {
    try {
        $invoiceList = $pdo->prepare(
            'SELECT i.*,q.queue_no FROM `' . table('invoices') . '` i LEFT JOIN `' . table('queue_entries') . '` q ON q.id=i.queue_entry_id '
            . 'WHERE i.patient_id=? AND i.branch_id=? AND i.status IN ("unpaid","partial") ORDER BY i.created_at DESC,i.id DESC LIMIT 30'
        );
        $invoiceList->execute([$selectedPatientId,$branchId]);
        $selectedPatientInvoices = $invoiceList->fetchAll();
    } catch (Throwable $accountLoadError) {
        error_log('Cashier patient account compatibility fallback: ' . $accountLoadError->getMessage());
        $cashierSchemaIssues[] = 'patient invoice account query requires database repair';
        $selectedPatientInvoices = [];
    }
    $selectedEncounterCandidates = cashier_direct_encounter_candidates($pdo,$selectedPatientId,$branchId);

    if ($requestedInvoiceId > 0) {
        $stmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE id=? AND patient_id=? AND branch_id=?');
        $stmt->execute([$requestedInvoiceId,$selectedPatientId,$branchId]);
        $selectedInvoice = $stmt->fetch() ?: null;
    }
    if (!$selectedInvoice && $selectedQueue && !empty($selectedQueue['latest_invoice_id'])) {
        $stmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE id=? AND branch_id=?');
        $stmt->execute([(int)$selectedQueue['latest_invoice_id'],$branchId]);
        $selectedInvoice = $stmt->fetch() ?: null;
    }
    if (!$selectedInvoice && !$selectedQueue && $requestedEncounterId < 1 && $selectedPatientInvoices) {
        $selectedInvoice = $selectedPatientInvoices[0];
    }

    if ($selectedInvoice && !empty($selectedInvoice['encounter_id'])) {
        $selectedEncounterId = (int)$selectedInvoice['encounter_id'];
    } elseif ($requestedEncounterId > 0) {
        foreach ($selectedEncounterCandidates as $candidate) {
            if ((int)$candidate['id'] === $requestedEncounterId) {
                $selectedEncounterId = $requestedEncounterId;
                break;
            }
        }
    } elseif ($selectedEncounterId < 1 && $selectedEncounterCandidates) {
        $selectedEncounterId = (int)$selectedEncounterCandidates[0]['id'];
    }

    if ($selectedInvoice && $posReady) {
        $stmt = $pdo->prepare('SELECT * FROM `' . table('invoice_items') . '` WHERE invoice_id=? ORDER BY id');
        $stmt->execute([(int)$selectedInvoice['id']]);
        $selectedItems = $stmt->fetchAll();
        if (!$selectedItems) {
            $fallbackAmount = max(0, (float)($selectedInvoice['amount'] ?? 0));
            $selectedItems[] = [
                'description'=>trim((string)($selectedInvoice['description'] ?? '')) ?: 'Existing account balance',
                'quantity'=>1,
                'unit_price'=>$fallbackAmount,
                'gross_amount'=>$fallbackAmount,
                'service_id'=>null,
                'treatment_plan_item_id'=>null,
                'procedure_id'=>null,
                'clinical_usage_item_id'=>null,
                'source_type'=>'existing_invoice',
            ];
        }
    }
}
$selectedReceiptPaymentId = 0;
$selectedInvoicePayments = [];
if ($selectedInvoice && cashier_schema_has_columns(table('payments'), ['id','invoice_id','amount','method'])) {
    try {
        $paymentOrder = schema_column_exists(table('payments'),'created_at') ? 'created_at DESC,id DESC' : 'id DESC';
        $createdSelect = schema_column_exists(table('payments'),'created_at') ? 'created_at' : 'NULL AS created_at';
        $referenceSelect = schema_column_exists(table('payments'),'reference_no') ? 'reference_no' : 'NULL AS reference_no';
        $statusSelect = schema_column_exists(table('payments'),'status') ? 'status' : '"posted" AS status';
        $paymentListStmt=$pdo->prepare(
            'SELECT id,amount,method,' . $createdSelect . ',' . $referenceSelect . ',' . $statusSelect
            . ' FROM `' . table('payments') . '` WHERE invoice_id=? ORDER BY ' . $paymentOrder . ' LIMIT 50'
        );
        $paymentListStmt->execute([(int)$selectedInvoice['id']]);
        $selectedInvoicePayments=$paymentListStmt->fetchAll();
        if ($requestedPaymentId > 0) {
            foreach ($selectedInvoicePayments as $receiptPayment) {
                if ((int)$receiptPayment['id'] === $requestedPaymentId) {
                    $selectedReceiptPaymentId=$requestedPaymentId;
                    break;
                }
            }
        }
        if ($selectedReceiptPaymentId < 1 && $selectedInvoicePayments) {
            $selectedReceiptPaymentId=(int)$selectedInvoicePayments[0]['id'];
        }
    } catch (Throwable $receiptLookupError) {
        error_log('Cashier receipt history compatibility fallback: ' . $receiptLookupError->getMessage());
        $selectedInvoicePayments=[];
        $selectedReceiptPaymentId=0;
    }
}

$services = [];
if (cashier_schema_has_columns(table('service_catalog'), ['id','name','standard_fee','fee_status','is_active'])) {
    try {
        $descriptionSelect = schema_column_exists(table('service_catalog'),'description') ? 'description' : 'NULL description';
        $branchFilter = schema_column_exists(table('service_catalog'),'branch_id') ? ' AND (branch_id=? OR branch_id IS NULL)' : '';
        $stmt = $pdo->prepare('SELECT id,name,' . $descriptionSelect . ',standard_fee,fee_status FROM `' . table('service_catalog') . '` WHERE is_active=1 AND fee_status="billable" AND standard_fee>0' . $branchFilter . ' ORDER BY name');
        $stmt->execute($branchFilter !== '' ? [$branchId] : []);
        $services = $stmt->fetchAll();
    } catch (Throwable $serviceLoadError) {
        error_log('Cashier service catalogue compatibility fallback: ' . $serviceLoadError->getMessage());
        $cashierSchemaIssues[] = 'service catalogue query requires database repair';
    }
}
$retailProducts=[];
if(schema_column_exists(table('inventory_items'),'is_retail_product')&&schema_column_exists(table('inventory_items'),'selling_price')&&schema_table_exists(table('inventory_branch_stocks'))){
    try{
        $stmt=$pdo->prepare('SELECT i.id,i.sku,i.name,i.selling_price,i.unit,bs.current_stock FROM `' . table('inventory_items') . '` i JOIN `' . table('inventory_branch_stocks') . '` bs ON bs.item_id=i.id AND bs.branch_id=? WHERE i.status="active" AND i.is_retail_product=1 AND i.selling_price>0 AND bs.current_stock>0 AND (i.branch_id=? OR i.branch_id IS NULL) ORDER BY i.name');
        $stmt->execute([$branchId,$branchId]);$retailProducts=$stmt->fetchAll();
    }catch(Throwable $retailLoadError){error_log('Cashier retail product catalogue fallback: '.$retailLoadError->getMessage());}
}
$suggested = [];
$suggestedUsage = [];
$registerCompatibilityNotice = '';
if ($selectedEncounterId > 0) {
    try {
        $procedureTable = table('procedures');
        $invoiceItemTable = table('invoice_items');
        $invoiceTable = table('invoices');
        if (cashier_schema_has_columns($procedureTable, ['id','encounter_id','procedure_name','status'])
            && cashier_schema_has_columns($invoiceItemTable, ['invoice_id','procedure_id'])
            && cashier_schema_has_columns($invoiceTable, ['id','status'])) {
            $procedureCharge = cashier_procedure_charge_fragments('pr');
            $toothSelect = schema_column_exists($procedureTable, 'tooth_region') ? 'pr.tooth_region' : 'NULL tooth_region';
            $planSelect = schema_column_exists($procedureTable, 'treatment_plan_item_id') ? 'pr.treatment_plan_item_id' : 'NULL treatment_plan_item_id';
            $sql = 'SELECT pr.id procedure_id,' . $procedureCharge['name_expression'] . ' description,' . $toothSelect . ',' . $planSelect . ',' . $procedureCharge['service_select'] . ',' . $procedureCharge['fee_expression'] . ' unit_price,'
                . $procedureCharge['fee_status_select'] . ',' . $procedureCharge['no_charge_select'] . ',' . $procedureCharge['authorized_by_select'] . ',' . $procedureCharge['authorized_at_select'] . ',' . $procedureCharge['billing_status_select'] . ' '
                . 'FROM `' . $procedureTable . '` pr '
                . $procedureCharge['plan_join']
                . $procedureCharge['service_join']
                . 'WHERE pr.encounter_id=? AND pr.status="completed" AND NOT EXISTS (SELECT 1 FROM `' . $invoiceItemTable . '` ii JOIN `' . $invoiceTable . '` billed ON billed.id=ii.invoice_id WHERE ii.procedure_id=pr.id AND billed.status NOT IN ("voided","refunded")) ORDER BY pr.id';
            $stmt = $pdo->prepare($sql);
            $stmt->execute([$selectedEncounterId]);
            $suggested = $stmt->fetchAll();
        } else {
            $registerCompatibilityNotice = 'The procedure charge link requires the pending database repair.';
        }

        $usageTable = table('clinical_usage_items');
        $usageColumns = ['id','encounter_id','description','quantity','unit_price','line_total','usage_type','billing_mode','fulfillment_status'];
        if (cashier_schema_has_columns($usageTable, $usageColumns)
            && cashier_schema_has_columns($invoiceItemTable, ['invoice_id','clinical_usage_item_id'])
            && cashier_schema_has_columns($invoiceTable, ['id','status'])) {
            $inventoryJoin = '';
            $skuSelect = 'NULL sku';
            if (schema_column_exists($usageTable, 'inventory_item_id')
                && cashier_schema_has_columns(table('inventory_items'), ['id','sku'])) {
                $inventoryJoin = ' LEFT JOIN `' . table('inventory_items') . '` i ON i.id=cu.inventory_item_id ';
                $skuSelect = 'i.sku';
            }
            $usageStmt = $pdo->prepare(
                'SELECT cu.*,' . $skuSelect . ' FROM `' . $usageTable . '` cu' . $inventoryJoin
                . 'WHERE cu.encounter_id=? AND cu.billing_mode="bill_separately" AND cu.line_total>0 AND cu.fulfillment_status IN ("requested","confirmed") '
                . 'AND NOT EXISTS (SELECT 1 FROM `' . $invoiceItemTable . '` ii JOIN `' . $invoiceTable . '` billed ON billed.id=ii.invoice_id WHERE ii.clinical_usage_item_id=cu.id AND billed.status NOT IN ("voided","refunded")) ORDER BY cu.id'
            );
            $usageStmt->execute([$selectedEncounterId]);
            $suggestedUsage = $usageStmt->fetchAll();
        }
    } catch (Throwable $registerSuggestionError) {
        error_log('Cashier register suggestion compatibility fallback: ' . $registerSuggestionError->getMessage());
        $registerCompatibilityNotice = 'The clinical charge preview requires the pending database repair.';
        $suggested = [];
        $suggestedUsage = [];
    }

    // Suggestions already exclude anything billed on the current or another active invoice.
    // Merge every still-unbilled clinical source into the register, even when the Dentist
    // already prepared an invoice containing only the primary procedure.
    foreach ($suggested as $item) {
        $description = trim((string)$item['description'] . (!empty($item['tooth_region']) ? ' · ' . (string)$item['tooth_region'] : ''));
        $feeStatus=(string)($item['fee_status']??'blocked');
        $noChargeReason=trim((string)($item['no_charge_reason']??''));
        $selectedItems[] = ['description'=>$description,'quantity'=>1,'unit_price'=>(float)$item['unit_price'],'gross_amount'=>(float)$item['unit_price'],'service_id'=>(int)($item['service_id'] ?? 0) ?: null,'treatment_plan_item_id'=>(int)($item['treatment_plan_item_id'] ?? 0) ?: null,'procedure_id'=>(int)$item['procedure_id'],'clinical_usage_item_id'=>null,'source_type'=>'clinical_procedure','fee_status'=>$feeStatus,'no_charge_reason'=>$noChargeReason,'notes'=>$feeStatus==='no_charge'?'Authorized no-charge: '.$noChargeReason:null];
    }
    foreach ($suggestedUsage as $item) {
        $selectedItems[]=['description'=>(string)$item['description'],'quantity'=>(float)$item['quantity'],'unit_price'=>(float)$item['unit_price'],'gross_amount'=>(float)$item['line_total'],'service_id'=>null,'treatment_plan_item_id'=>null,'procedure_id'=>null,'clinical_usage_item_id'=>(int)$item['id'],'source_type'=>match((string)$item['usage_type']){'medicine_dispense'=>'dispensed_medicine','equipment'=>'equipment_service','diagnostic_service'=>'diagnostic_service',default=>'clinical_supply'}];
    }
}

$approvedDiscounts = [];
$approvedZeroCharges = [];
if ($posReady && $selectedPatientId > 0) {
    $stmt = $pdo->prepare('SELECT * FROM `' . table('discount_authorizations') . '` WHERE branch_id=? AND patient_id=? AND status="approved" AND (queue_entry_id IS NULL OR queue_entry_id=?) ORDER BY reviewed_at DESC');
    $stmt->execute([$branchId,$selectedPatientId,$selectedQueue ? (int)$selectedQueue['id'] : 0]); $approvedDiscounts = $stmt->fetchAll();
    if ($selectedQueue) {
        $stmt = $pdo->prepare('SELECT * FROM `' . table('zero_charge_authorizations') . '` WHERE branch_id=? AND patient_id=? AND queue_entry_id=? AND status="approved" ORDER BY reviewed_at DESC');
        $stmt->execute([$branchId,$selectedPatientId,(int)$selectedQueue['id']]); $approvedZeroCharges = $stmt->fetchAll();
    }
}

$selectedRegisterPreview = $selectedEncounterId > 0 ? cashier_encounter_charge_preview($pdo,$selectedEncounterId) : ['items'=>[],'total'=>0.0,'procedure_total'=>0.0,'clinical_usage_total'=>0.0,'unbilled_total'=>0.0,'missing_fee_count'=>0,'unbilled_count'=>0,'summary'=>'No finalized procedure recorded'];
$selectedQueueInvoiceCount = 0;
$selectedQueueTotalCharges = 0.0;
$selectedQueueBalance = 0.0;
$selectedQueueInsuranceReceivable = 0.0;
$selectedQueuePatientResponsibility = 0.0;
$selectedQueueCompletionMode = 'blocked';
if ($selectedQueue && $posReady) {
    $stmt = $pdo->prepare('SELECT COUNT(*) invoice_count,COALESCE(SUM(amount),0) total_charges,COALESCE(SUM(CASE WHEN status IN ("unpaid","partial") THEN amount-paid_amount ELSE 0 END),0) balance FROM `' . table('invoices') . '` WHERE queue_entry_id=? AND status NOT IN ("voided","refunded")');
    $stmt->execute([(int)$selectedQueue['id']]);
    $queueFinance = $stmt->fetch() ?: [];
    $selectedQueueInvoiceCount = (int)($queueFinance['invoice_count'] ?? 0);
    $selectedQueueTotalCharges = (float)($queueFinance['total_charges'] ?? 0);
    $selectedQueueBalance = (float)($queueFinance['balance'] ?? 0);
    if ($selectedQueueBalance > 0.001) {
        $stmt = $pdo->prepare('SELECT COALESCE(SUM(GREATEST(c.approved_amount-c.settled_amount,0)),0) FROM `' . table('insurance_claims') . '` c JOIN `' . table('invoices') . '` i ON i.id=c.invoice_id WHERE i.queue_entry_id=? AND i.branch_id=? AND c.status IN ("approved","partially_approved")');
        $stmt->execute([(int)$selectedQueue['id'],$branchId]);
        $selectedQueueInsuranceReceivable = (float)$stmt->fetchColumn();
    }
    $selectedQueuePatientResponsibility = max(0,round($selectedQueueBalance-$selectedQueueInsuranceReceivable,2));
    if ((int)($selectedRegisterPreview['missing_fee_count'] ?? 0) > 0) $selectedQueueCompletionMode = 'billing_correction';
    elseif ($selectedQueueInvoiceCount === 0 || $selectedQueueTotalCharges <= 0.001) $selectedQueueCompletionMode = 'zero_charge';
    elseif ($selectedQueuePatientResponsibility <= 0.001 && $selectedQueueBalance > 0.001) $selectedQueueCompletionMode = 'insurance';
    elseif ($selectedQueueBalance <= 0.001) $selectedQueueCompletionMode = 'settled';
}

$paymentEmailReady = schema_column_exists(table('payments'), 'receipt_email_outbox_id') && schema_table_exists(table('email_outbox'));
$receiptSelect = $paymentEmailReady ? ',pay.receipt_email_outbox_id,eo.status receipt_delivery_status' : ',NULL receipt_email_outbox_id,NULL receipt_delivery_status';
$receiptJoin = $paymentEmailReady ? ' LEFT JOIN `' . table('email_outbox') . '` eo ON eo.id=pay.receipt_email_outbox_id' : '';
$latestAdjustedSelect = schema_column_exists(table('payments'), 'adjustment_total') ? 'pay.adjustment_total' : '0';
$latestStatusSelect = schema_column_exists(table('payments'), 'status') ? 'pay.status' : '"posted"';
$latestSourceSelect = schema_column_exists(table('payments'), 'source_type') ? 'pay.source_type' : '"patient"';
$latestPayerSelect = cashier_schema_has_columns(table('payments'), ['payer_name','payer_relationship']) ? 'pay.payer_name latest_payer_name,pay.payer_relationship latest_payer_relationship,' : 'NULL latest_payer_name,NULL latest_payer_relationship,';
$invoiceRows=[];$invoiceRowsTotal=0;$managedReturnInvoice=null;
$txState=module_management_state('tx_page','tx_per_page',25);$txPage=$txState['page'];$txPerPage=$txState['per_page'];$txOffset=$txState['offset'];$returnPaymentId=max(0,(int)($_GET['return_payment_id']??0));
$payableQuery=trim((string)($_GET['payable_q']??''));
$payableStatus=(string)($_GET['payable_status']??'open');
if(!in_array($payableStatus,['open','all','paid'],true))$payableStatus='open';
try{
    $invoiceSelect='SELECT i.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,p.mobile,p.email,q.queue_no,' . $latestPayerSelect . 'pay.id latest_payment_id,pay.amount latest_payment_amount,' . $latestAdjustedSelect . ' latest_payment_adjusted,pay.method latest_payment_method,pay.reference_no latest_payment_reference,' . $latestStatusSelect . ' latest_payment_status,' . $latestSourceSelect . ' latest_payment_source,' . ($posReady ? '(SELECT pa2.id FROM `' . table('payment_adjustments') . '` pa2 WHERE pa2.invoice_id=i.id AND pa2.status="processed" ORDER BY pa2.processed_at DESC,pa2.id DESC LIMIT 1)' : 'NULL') . ' latest_adjustment_id' . $receiptSelect . ' FROM `' . table('invoices') . '` i JOIN `' . table('patients') . '` p ON p.id=i.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=i.queue_entry_id LEFT JOIN `' . table('payments') . '` pay ON pay.id=(SELECT p2.id FROM `' . table('payments') . '` p2 WHERE p2.invoice_id=i.id ORDER BY p2.id DESC LIMIT 1)' . $receiptJoin;
    $payableWhere=['i.branch_id=?'];$payableArgs=[$branchId];
    if($payableStatus==='open')$payableWhere[]='i.status IN ("unpaid","partial")';
    elseif($payableStatus==='paid')$payableWhere[]='i.status="paid"';
    else $payableWhere[]='i.status NOT IN ("voided","refunded","written_off")';
    if($payableQuery!==''){
        $needle='%'.$payableQuery.'%';
        $payableWhere[]='(i.invoice_no LIKE ? OR p.patient_no LIKE ? OR p.first_name LIKE ? OR p.last_name LIKE ? OR CONCAT(p.first_name," ",p.last_name) LIKE ? OR p.mobile LIKE ? OR p.email LIKE ? OR q.queue_no LIKE ? OR pay.reference_no LIKE ?)';
        array_push($payableArgs,$needle,$needle,$needle,$needle,$needle,$needle,$needle,$needle,$needle);
    }
    $whereSql=' WHERE '.implode(' AND ',$payableWhere);
    $countSql='SELECT COUNT(*) FROM `' . table('invoices') . '` i JOIN `' . table('patients') . '` p ON p.id=i.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=i.queue_entry_id LEFT JOIN `' . table('payments') . '` pay ON pay.id=(SELECT p2.id FROM `' . table('payments') . '` p2 WHERE p2.invoice_id=i.id ORDER BY p2.id DESC LIMIT 1)' . $receiptJoin . $whereSql;
    $countStmt=$pdo->prepare($countSql);$countStmt->execute($payableArgs);$invoiceRowsTotal=(int)$countStmt->fetchColumn();
    $invoiceRowsStmt=$pdo->prepare($invoiceSelect.$whereSql.' ORDER BY (i.status IN ("unpaid","partial")) DESC,i.created_at DESC LIMIT '.$txPerPage.' OFFSET '.$txOffset);$invoiceRowsStmt->execute($payableArgs);$invoiceRows=$invoiceRowsStmt->fetchAll();
    if($returnPaymentId>0){$managedStmt=$pdo->prepare($invoiceSelect.' WHERE i.branch_id=? AND pay.id=? LIMIT 1');$managedStmt->execute([$branchId,$returnPaymentId]);$managedReturnInvoice=$managedStmt->fetch()?:null;}
}catch(Throwable $recentLoadError){error_log('Cashier payable list compatibility fallback: '.$recentLoadError->getMessage());$cashierSchemaIssues[]='cashier payable list query requires database repair';}
$selectedQueueCalled = !$selectedQueue || (string)($selectedQueue['status'] ?? '') === 'cashier_called';
$transactionActive=$activeSession&&$selectedPatientId>0&&(int)($activeSession['locked_patient_id']??0)===$selectedPatientId&&((int)($activeSession['locked_queue_entry_id']??0)===0||(int)($activeSession['locked_queue_entry_id']??0)===(int)($selectedQueue['id']??0));
$selectedChargeLocked = $selectedInvoice && (!empty($selectedInvoice['charge_locked_at'] ?? null) || (float)($selectedInvoice['paid_amount'] ?? 0) > 0);
$selectedOutstandingBalance = 0.0;
foreach ($selectedPatientInvoices as $openInvoice) $selectedOutstandingBalance += max(0,(float)$openInvoice['amount']-(float)$openInvoice['paid_amount']);
$selectedOutstandingBalance = round($selectedOutstandingBalance,2);
$selectedInvoiceBalance = $selectedInvoice ? max(0, round((float)$selectedInvoice['amount'] - (float)$selectedInvoice['paid_amount'], 2)) : 0.0;
$selectedBalanceOnly = $selectedPatient && !$selectedQueue && $selectedInvoice && $selectedInvoiceBalance > 0.001;
if ($selectedBalanceOnly) {
    $selectedChargeLocked = true;
}
$defaultPayerName = $selectedPatient ? trim((string)$selectedPatient['first_name'] . ' ' . (string)$selectedPatient['last_name']) : '';
$cashierRoleReportError = null;
$cashierRoleReport = ['payments_today'=>0,'amount_today'=>0.0,'pending_returns'=>0,'session_rows'=>[]];
if (in_array(active_role(), ['cashier','staff'], true)) {
    try {
        $cashierRoleReport = dental_cashier_my_report($pdo, $branchId, $userId);
    } catch (Throwable $reportFailure) {
        $cashierRoleReportError = $reportFailure->getMessage();
        error_log('DentalCare cashier report degraded: ' . $reportFailure->getMessage());
    }
}
$checkoutRows=[];
if($posReady){
    $checkoutSql='SELECT c.*,u.name cashier_name,w.window_name,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no,q.queue_no FROM `' . table('cashier_checkouts') . '` c JOIN `' . table('users') . '` u ON u.id=c.cashier_user_id JOIN `' . table('cashier_windows') . '` w ON w.id=c.cashier_window_id JOIN `' . table('patients') . '` p ON p.id=c.patient_id LEFT JOIN `' . table('queue_entries') . '` q ON q.id=c.queue_entry_id WHERE c.branch_id=?';
    $checkoutArgs=[$branchId];
    if(!$isSupervisor){$checkoutSql.=' AND c.cashier_user_id=?';$checkoutArgs[]=$userId;}
    $checkoutSql.=' ORDER BY c.completed_at DESC,c.id DESC LIMIT 50';
    $checkoutStmt=$pdo->prepare($checkoutSql);$checkoutStmt->execute($checkoutArgs);$checkoutRows=$checkoutStmt->fetchAll();
}

$completedPayment = null;
$completedPaymentId = max(0,(int)($_GET['completed_payment_id'] ?? 0));
if ($completedPaymentId > 0) {
    try {
        $completedSql = 'SELECT pay.id payment_id,pay.amount payment_amount,pay.method,pay.reference_no,pay.created_at,pay.received_by,'
            . 'i.id invoice_id,i.invoice_no,i.patient_id,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no '
            . 'FROM `' . table('payments') . '` pay JOIN `' . table('invoices') . '` i ON i.id=pay.invoice_id '
            . 'JOIN `' . table('patients') . '` p ON p.id=i.patient_id WHERE pay.id=? AND i.branch_id=?';
        $completedArgs = [$completedPaymentId,$branchId];
        if (!$isSupervisor) { $completedSql .= ' AND pay.received_by=?'; $completedArgs[]=$userId; }
        $completedSql .= ' LIMIT 1';
        $completedStmt=$pdo->prepare($completedSql);$completedStmt->execute($completedArgs);$completedPayment=$completedStmt->fetch()?:null;
    } catch (Throwable $completedReceiptError) {
        error_log('Completed cashier receipt summary fallback: ' . $completedReceiptError->getMessage());
    }
}

render_header('Billing & Cashier Register');
if ($completedPayment) {
    echo '<section class="card cashier-completed-task"><div><span class="eyebrow">Transaction complete</span><h2>' . e((string)$completedPayment['patient']) . ' · ' . e((string)$completedPayment['invoice_no']) . '</h2><p>Payment of ₱' . number_format((float)$completedPayment['payment_amount'],2) . ' was posted. The patient context and POS lock have been released.</p></div><div class="status-actions"><a class="btn primary" target="_blank" href="print_record.php?type=receipt&amp;id=' . (int)$completedPayment['invoice_id'] . '&amp;payment_id=' . (int)$completedPayment['payment_id'] . '">Print receipt</a><a class="btn ghost" href="billing.php">Next patient</a></div></section>';
}
if (!$selectedPatient && function_exists('dentalcare_render_role_lane_card')) dentalcare_render_role_lane_card('cashier', 'cashier-pos-flow-card');
if (!$selectedPatient && $activeSession && (int)($activeSession['locked_patient_id'] ?? 0) > 0) echo '<section class="card cashier-pos-transaction-guard"><div class="card-head"><div><span class="eyebrow">Active POS lock</span><h2>Finish or cancel the current patient transaction first</h2><p>This register is intentionally locked to one patient/payee so payment cannot be posted to the wrong account.</p></div><span class="badge warning">Locked</span></div></section>';
if ($error !== '') echo '<div class="alert danger"><strong>Billing transaction was not completed.</strong><br>' . e($error) . '</div>';
if (!$posReady) echo '<div class="alert warning"><strong>Cashier database update required.</strong> Open Superuser Settings → Updates &amp; Database before receiving payments.</div>';
if ($cashierSchemaIssues) echo '<div class="alert warning"><strong>Cashier Register compatibility repair is pending.</strong><br>The page will keep available account information visible, but receiving payment remains disabled until the Superuser applies Version 0.5.9.8.14.<br><small>' . e(implode(' · ', array_unique($cashierSchemaIssues))) . '</small></div>';
if ($registerCompatibilityNotice !== '') echo '<div class="alert warning"><strong>Clinical charge preview is temporarily limited.</strong><br>' . e($registerCompatibilityNotice) . '</div>';
?>
<?php $routineExceptionCount=$exceptionTotal; ?>
<div class="finance-view-switch" role="navigation" aria-label="Finance workspace">
  <a href="billing.php" class="active">Cashier Register</a>
  <?php if($isSupervisor):?><a href="billing.php?view=exceptions">Cashier Exceptions<?php if($routineExceptionCount>0):?> <span><?=$routineExceptionCount?></span><?php endif;?></a><?php endif;?>
</div>
<div class="pos-page-heading">
  <div><span class="eyebrow">Finance workspace</span><h2>Cashier Register</h2><p>Work from the queue or patient search first. Reports and history are below the active transaction area.</p></div>
  <div class="pos-patient-search" data-patient-picker data-cashier-patient-picker data-endpoint="<?=e($cashierPatientSearchEndpoint)?>">
    <form method="post" data-cashier-lock-form hidden><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="lock_transaction"><input type="hidden" name="patient_id" data-cashier-lock-patient></form>
    <div class="remote-patient-picker">
      <label for="cashier-patient-search">Patient or payee account</label>
      <input id="cashier-patient-search" type="search" placeholder="<?= $lockedPatientId>0?'Complete or cancel the active transaction first':'Patient no., full name, guardian/payee, mobile, or email' ?>" data-patient-query autocomplete="off" <?=$lockedPatientId>0?'disabled':''?>>
      <input type="hidden" data-patient-id>
      <div class="remote-picker-selection" data-patient-selection hidden></div>
      <div class="remote-picker-results" data-patient-results hidden></div>
    </div>
  </div>
</div>
<details class="card pos-session-card" <?=!$activeSession?'open':''?>>
<summary><span><strong><?= $activeSession?e((string)($activeSession['window_name']?:'Cashier window')).' · Session #'.(int)$activeSession['id']:'Open cashier session' ?></strong><small><?= $activeSession?'Drawer is ready to receive payments.':'A session is required before taking payment.' ?></small></span><span class="badge"><?= $activeSession?'Open':'Required' ?></span></summary>
<div class="pos-session-body"><div class="card-head"><div><h2>Cashier session</h2><span class="muted">Every payment and return is tied to a reconciled drawer session.</span></div></div>
<?php if ($activeSession): $expected=(float)$activeSession['opening_cash']+$sessionTotals['cash_received']-$sessionTotals['cash_returned']; ?>
<div class="stats mini-stats"><?php stat_card('Opening cash','₱'.number_format((float)$activeSession['opening_cash'],2),'Session #'.(int)$activeSession['id']);stat_card('Cash received','₱'.number_format($sessionTotals['cash_received'],2),'Posted payments');stat_card('Cash returned','₱'.number_format($sessionTotals['cash_returned'],2),'Processed refunds');stat_card('Expected drawer','₱'.number_format($expected,2),'Before physical count');?></div>
<form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="close_session"><label>Physical cash counted<input type="number" min="0" step="0.01" name="counted_cash" required></label><label class="wide">Closing notes<textarea name="close_notes" placeholder="Shift handoff, terminal issue, or variance explanation"></textarea></label><button class="btn primary">Close and reconcile session</button></form>
<?php else: ?>
<form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="open_session"><label>Cashier window<select name="window_id" required><option value="">Choose window</option><?php foreach($cashierWindows as $window):?><option value="<?=(int)$window['id']?>"><?=e((string)$window['window_name'])?> · <?=e((string)$window['window_code'])?></option><?php endforeach;?></select></label><label>Opening cash<input type="number" min="0" step="0.01" name="opening_cash" value="0" required></label><button class="btn primary" <?=($posReady&&$cashierWindows)?'':'disabled'?>>Open cashier session</button></form><?php if(!$cashierWindows):?><div class="alert warning">No active cashier window exists. An Administrator must add one.</div><?php endif;?><?php if($isSupervisor):?><details><summary>Add cashier window</summary><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create_window"><label>Window name<input name="window_name" required placeholder="Cashier Window 2"></label><label>Window code<input name="window_code" required placeholder="CW2"></label><button class="btn ghost">Add window</button></form></details><?php endif;?>
<?php endif; ?></div></details>


<?php if($isSupervisor && $staleOpenSessions):?>
<section class="card" id="stale-cashier-sessions">
  <div class="card-head"><div><span class="eyebrow">Supervisor recovery</span><h2>Stale cashier sessions requiring drawer reconciliation</h2><p>These sessions contain operational or financial evidence, so DentalCare will not auto-close them. Verify the physical or documented drawer amount and close each session here.</p></div><span class="badge warning"><?=count($staleOpenSessions)?> open</span></div>
  <div class="table-wrap"><table><thead><tr><th>Cashier / window</th><th>Opened</th><th>Evidence</th><th>Expected drawer</th><th>Locks</th><th>Reconcile</th></tr></thead><tbody>
  <?php foreach($staleOpenSessions as $staleSession):$staleTotals=(array)($staleSession['totals']??[]);$staleLocked=(int)($staleSession['locked_patient_id']??0)>0||(int)($staleSession['locked_queue_entry_id']??0)>0;?>
  <tr><td><strong><?=e((string)($staleSession['cashier_name']?:'Unknown cashier'))?></strong><small><?=e((string)($staleSession['resolved_window_name']?:'Window not recorded'))?> · Session #<?=(int)$staleSession['id']?></small></td><td><?=e(date('M j, Y g:i A',strtotime((string)$staleSession['opened_at'])))?></td><td><strong>Financial evidence retained</strong><small>Cash in ₱<?=number_format((float)($staleTotals['cash_received']??0),2)?> · Non-cash ₱<?=number_format((float)($staleTotals['noncash_received']??0),2)?> · Cash out ₱<?=number_format((float)($staleTotals['cash_returned']??0),2)?></small></td><td><strong>₱<?=number_format((float)$staleSession['expected_cash_preview'],2)?></strong><small>Opening ₱<?=number_format((float)$staleSession['opening_cash'],2)?></small></td><td><?php if($staleLocked):?><span class="badge danger">Patient lock remains</span><small>Resolve the focused transaction first.</small><?php else:?><span class="badge success">Clear</span><?php endif;?></td><td><?php if(!$staleLocked):?><details><summary class="btn ghost compact">Reconcile</summary><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="supervisor_reconcile_stale_session"><input type="hidden" name="session_id" value="<?=(int)$staleSession['id']?>"><label>Physical or verified cash<input type="number" min="0" step="0.01" name="counted_cash" value="<?=e(number_format((float)$staleSession['expected_cash_preview'],2,'.',''))?>" required></label><label>Reconciliation note<textarea name="reconciliation_note" minlength="10" required placeholder="State how the drawer amount was verified and why the session remained open."></textarea></label><button class="btn primary">Close stale session</button></form></details><?php else:?><a class="btn ghost compact" href="production-monitoring.php#historical-operational-state">Review lock</a><?php endif;?></td></tr>
  <?php endforeach;?>
  </tbody></table></div>
</section>
<?php endif;?>

<?php if($isSupervisor && $routineExceptionCount>0):?>
<section class="cashier-routine-exception-link"><div><span class="eyebrow">Supervisor action required</span><strong><?=$routineExceptionCount?> cashier exception<?=$routineExceptionCount===1?'':'s'?> waiting</strong><small>Approvals and returns are kept outside the routine collection screen.</small></div><a class="btn primary" href="billing.php?view=exceptions">Open Cashier Exceptions</a></section>
<?php endif;?>

<div class="pos-shell">
<aside class="pos-queue-rail">
  <div class="pos-panel-head" data-live-queue-region="cashier-queue-summary"><div><strong>Waiting for cashier</strong><span><?=count($cashierRows)?> patient<?=count($cashierRows)===1?'':'s'?></span></div><span class="badge">Today</span></div>
  <div class="pos-queue-search"><input type="search" placeholder="Filter queue by name or number" data-pos-queue-search autocomplete="off"></div>
  <div class="pos-queue-list" data-pos-queue-list data-live-queue-region="cashier-queue-list">
  <?php foreach($cashierRows as $row):$active=$selectedQueue&&(int)$selectedQueue['id']===(int)$row['id'];$preview=$row['charge_preview']??[];$procedureTotal=(float)($preview['procedure_total']??0);$procedureSummary=(string)($preview['summary']??'No finalized procedure recorded');$queueAction=(string)$row['status']==='waiting_billing'?'call_cashier':'lock_transaction'; ?>
    <article class="pos-queue-ticket <?=e((string)$row['status'])?> <?=$active?'selected':''?>" data-pos-queue-ticket data-search="<?=e(mb_strtolower((string)$row['queue_no'].' '.$row['patient'].' '.$row['patient_no'].' '.$procedureSummary))?>">
      <form method="post" class="pos-queue-select-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="<?=e($queueAction)?>"><input type="hidden" name="patient_id" value="<?=(int)$row['patient_id']?>"><input type="hidden" name="queue_entry_id" value="<?=(int)$row['id']?>"><button type="submit" class="pos-queue-select" <?=($lockedPatientId>0&&((int)$row['patient_id']!==$lockedPatientId||($lockedQueueId>0&&(int)$row['id']!==$lockedQueueId)))?'disabled':''?>>
        <span class="pos-queue-number"><?=e($row['queue_no'])?></span>
        <strong><?=e($row['patient'])?></strong>
        <small><?=e($row['patient_no'])?> · <?=e(module_status($row['status']))?></small>
        <em><?=e($procedureSummary)?></em>
        <span class="pos-queue-charge"><small>Procedure amount</small><b>₱<?=number_format($procedureTotal,2)?></b></span>
        <?php if((int)($preview['missing_fee_count']??0)>0):?><span class="pos-fee-warning">Fee needs Administrator/Dentist setup</span><?php endif;?>
        <span class="pos-call-hint"><?= $queueAction==='call_cashier'?'Call patient and open transaction':'Open focused transaction' ?></span>
      </button></form>
    </article>
  <?php endforeach;?>
  <?php if(!$cashierRows):?><div class="pos-empty">No patients are waiting for the cashier. Use the patient search above to collect an existing balance.</div><?php endif;?>
  </div>
</aside>

<?php if($selectedPatient):?>
<div class="pos-register-workspace">
  <?php if($transactionActive):?><section class="cashier-transaction-lock"><div><span class="eyebrow">Focused transaction</span><strong><?=e((string)($activeSession['window_name']?:'Cashier window'))?> · <?=e($selectedPatient['first_name'].' '.$selectedPatient['last_name'])?></strong><small>Patient switching and cashier navigation remain blocked until checkout is completed or cancelled.</small></div><details><summary>Cancel transaction</summary><form method="post" class="compact-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="cancel_transaction"><textarea name="cancel_reason" minlength="5" required placeholder="Reason for cancelling this cashier transaction"></textarea><button class="btn danger">Cancel without posting payment</button></form></details></section><?php elseif($activeSession):?><section class="alert warning"><strong>Start this transaction before editing charges or taking payment.</strong><form method="post" class="inline-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="lock_transaction"><input type="hidden" name="patient_id" value="<?=$selectedPatientId?>"><input type="hidden" name="queue_entry_id" value="<?=(int)($selectedQueue['id']??0)?>"><input type="hidden" name="invoice_id" value="<?=(int)($selectedInvoice['id']??0)?>"><input type="hidden" name="balance_only" value="<?=$selectedBalanceOnly?1:0?>"><input type="hidden" name="encounter_id" value="<?=$selectedBalanceOnly?0:$selectedEncounterId?>"><button class="btn primary"><?=$selectedBalanceOnly?'Start balance payment':'Start focused transaction'?></button></form></section><?php endif;?>
  <?php if (function_exists('dentalcare_render_cashier_enforcement_panel')) dentalcare_render_cashier_enforcement_panel($selectedPatient ?: null, $selectedInvoice ?: null, $selectedItems, $selectedQueue ?: null, $selectedQueueCompletionMode, (bool)$transactionActive); ?>
  <?php if(count($selectedPlannedServices)>1):?>
  <section class="card cashier-planned-services">
    <div class="card-head"><div><h3>Planned services for this visit</h3><p>Each procedure remains itemized for clinical completion and billing.</p></div><span class="badge"><?=count($selectedPlannedServices)?> services</span></div>
    <div class="appointment-service-segments"><?php foreach($selectedPlannedServices as $segment):?><small><span><?=e((string)$segment['service_name'])?></span><span><?=e(date('g:i A',strtotime((string)$segment['start_at'])))?>–<?=e(date('g:i A',strtotime((string)$segment['end_at'])))?><?php $segmentProvider=trim((string)(($segment['dentist_name']??'')?:($segment['inhouse_provider_name']??'')));?><?= $segmentProvider!==''?' · '.e($segmentProvider):''?></span></small><?php endforeach;?></div>
  </section>
  <?php endif;?>
  <section class="pos-account-selector">
    <div><span class="eyebrow">Payable account</span><strong><?=e($selectedPatient['first_name'].' '.$selectedPatient['last_name'])?></strong><small><?=e($selectedPatient['patient_no'])?> · Open balance ₱<?=number_format($selectedOutstandingBalance,2)?></small></div>
    <div class="pos-account-links">
      <?php foreach($selectedPatientInvoices as $openInvoice):$openBalance=max(0,(float)$openInvoice['amount']-(float)$openInvoice['paid_amount']);?><a class="<?=($selectedInvoice&&(int)$selectedInvoice['id']===(int)$openInvoice['id'])?'active':''?>" href="billing.php?patient_id=<?=$selectedPatientId?>&amp;invoice_id=<?=(int)$openInvoice['id']?>&amp;balance_only=1"><span><?=e($openInvoice['invoice_no'])?></span><b>Balance ₱<?=number_format($openBalance,2)?></b></a><?php endforeach;?>
      <?php foreach($selectedEncounterCandidates as $candidate):$candidatePreview=$candidate['charge_preview'];?><a class="<?=(!$selectedInvoice&&$selectedEncounterId===(int)$candidate['id'])?'active':''?>" href="billing.php?patient_id=<?=$selectedPatientId?>&amp;encounter_id=<?=(int)$candidate['id']?>"><span>Visit <?=e(date('M j, Y',strtotime((string)($candidate['finalized_at']?:$candidate['created_at']))))?></span><b>Unbilled ₱<?=number_format((float)$candidatePreview['unbilled_total'],2)?></b></a><?php endforeach;?>
      <?php if(!$selectedPatientInvoices&&!$selectedEncounterCandidates):?><span class="muted">No open invoice or unbilled finalized visit.</span><?php endif;?>
    </div>
  </section>

  <?php if($selectedBalanceOnly):?><section class="alert info"><strong>Balance-only payment.</strong> No check-in, queue ticket, appointment, or clinical encounter will be created. Cashier is collecting against the existing invoice balance only.</section><?php endif;?>
  <?php if($selectedQueue):?><form method="post" id="cashier-finalize-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="complete_cashier"><input type="hidden" name="queue_entry_id" value="<?=(int)$selectedQueue['id']?>"></form><form method="post" id="cashier-correction-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="return_billing_correction"><input type="hidden" name="patient_id" value="<?=(int)$selectedPatientId?>"><input type="hidden" name="queue_entry_id" value="<?=(int)$selectedQueueId?>"></form><?php endif;?>

  <form method="post" class="pos-transaction-grid" data-pos-register data-pos-charges-locked="<?=$selectedChargeLocked?'1':'0'?>">
    <input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="pos_checkout"><input type="hidden" name="payment_request_token" value="<?=e($paymentRequestToken)?>"><input type="hidden" name="patient_id" value="<?=$selectedPatientId?>"><input type="hidden" name="queue_entry_id" value="<?=$selectedBalanceOnly?0:(int)($selectedQueue['id']??0)?>"><input type="hidden" name="balance_only" value="<?=$selectedBalanceOnly?1:0?>"><input type="hidden" name="encounter_id" value="<?=$selectedBalanceOnly?0:$selectedEncounterId?>"><input type="hidden" name="invoice_id" value="<?=(int)($selectedInvoice['id']??0)?>">

    <section class="pos-register-panel">
      <div class="pos-customer-card"><div><span>Patient account</span><h3><?=e($selectedPatient['first_name'].' '.$selectedPatient['last_name'])?></h3><p><?=e($selectedPatient['patient_no'])?><?=!empty($selectedQueue['queue_no'])?' · Queue '.e($selectedQueue['queue_no']):($selectedBalanceOnly?' · Balance-only payment · no checkup':' · Direct account payment')?></p></div><div class="pos-customer-meta"><span>₱<?=number_format((float)($selectedInvoice ? max(0,(float)$selectedInvoice['amount']-(float)$selectedInvoice['paid_amount']) : $selectedRegisterPreview['unbilled_total']),2)?></span><small><?= $selectedInvoice?'Current invoice balance':'Finalized procedure amount' ?></small></div></div>
      <?php if($selectedInvoice):?><div class="pos-existing-invoice"><strong><?=e($selectedInvoice['invoice_no'])?></strong><span><?=e(module_status((string)$selectedInvoice['status']))?> · Created <?=e(date('M j, Y g:i A',strtotime((string)$selectedInvoice['created_at'])))?></span><?php if($selectedChargeLocked):?><em>Charges are locked because payment or approval activity already exists.</em><?php endif;?></div><?php endif;?>
      <?php if((int)$selectedRegisterPreview['missing_fee_count']>0):?><div class="alert danger"><strong>Billing-source correction required.</strong> A completed legacy procedure is not linked to an approved service or treatment-plan fee. Return the record to the Dentist or Administrator; Cashier cannot price it or treat the missing fee as a free visit.</div><?php endif;?>

      <div class="pos-cart-section">
        <div class="pos-section-title"><div><span class="eyebrow"><?=$selectedBalanceOnly?'Existing account receivable':'Dentist-finalized billing handoff'?></span><h3><?=$selectedBalanceOnly?'Open invoice balance':'Procedures and approved charges'?></h3></div><span><?=$selectedBalanceOnly?'Cashier receives payment only; charges are not edited and no checkup is created.':'Procedure descriptions and clinical amounts are read-only for Cashier.'?></span></div>
        <div class="pos-cart-table-wrap"><table class="pos-cart-table"><thead><tr><th>Procedure / item done</th><th>Qty</th><th>Approved amount</th><th>Line total</th><th></th></tr></thead><tbody data-pos-cart>
        <?php foreach($selectedItems as $item):$source=(string)($item['source_type']??((int)($item['procedure_id']??0)>0?'clinical_procedure':((int)($item['service_id']??0)>0?'service_catalog':'manual_charge')));$canonical=in_array($source,['clinical_procedure','service_catalog','retail_product','clinical_supply','dispensed_medicine','equipment_service','diagnostic_service'],true);$clinical=$source==='clinical_procedure'||(int)($item['clinical_usage_item_id']??0)>0;$sourceLabel=match($source){'clinical_procedure'=>'Procedure done','dispensed_medicine'=>'Medicine dispensed','clinical_supply'=>'Material used','equipment_service'=>'Equipment/service','diagnostic_service'=>'Diagnostic service','service_catalog'=>'Clinic service','retail_product'=>'Retail product',default=>'Authorized charge'};$authorizedNoCharge=(string)($item['fee_status']??'')==='no_charge'||str_starts_with((string)($item['notes']??''),'Authorized no-charge:');$retailInventoryId=($source==='retail_product'&&preg_match('/^RET-(\d+)$/',(string)($item['item_code']??''),$retailMatch))?(int)$retailMatch[1]:0; ?>
          <tr data-pos-row data-source-type="<?=e($source)?>"><td><span class="pos-line-source"><?=e($sourceLabel)?><?php if($authorizedNoCharge):?> · Authorized no-charge<?php endif;?></span><?php if($canonical||$selectedChargeLocked):?><strong class="pos-line-description"><?=e($item['description'])?></strong><input type="hidden" name="item_description[]" value="<?=e($item['description'])?>"><?php else:?><input name="item_description[]" value="<?=e($item['description'])?>" required><?php endif;?><input type="hidden" name="item_code[]" value="<?=e($item['item_code']??'')?>"><input type="hidden" name="item_service_id[]" value="<?=(int)($item['service_id']??0)?>"><input type="hidden" name="item_treatment_plan_item_id[]" value="<?=(int)($item['treatment_plan_item_id']??0)?>"><input type="hidden" name="item_procedure_id[]" value="<?=(int)($item['procedure_id']??0)?>"><input type="hidden" name="item_clinical_usage_id[]" value="<?=(int)($item['clinical_usage_item_id']??0)?>"><input type="hidden" name="item_inventory_id[]" value="<?=$retailInventoryId?>"></td><td><?php if($clinical||$selectedChargeLocked):?><span class="pos-readonly-number"><?=e(number_format((float)($item['quantity']??1),2))?></span><input type="hidden" name="item_quantity[]" value="<?=e(number_format((float)($item['quantity']??1),2,'.',''))?>" data-pos-quantity><?php else:?><input type="number" name="item_quantity[]" min="0.01" max="999" step="0.01" value="<?=e(number_format((float)($item['quantity']??1),2,'.',''))?>" data-pos-quantity><?php endif;?></td><td><?php if($canonical||$selectedChargeLocked):?><span class="pos-readonly-number">₱<?=number_format((float)($item['unit_price']??0),2)?></span><input type="hidden" name="item_unit_price[]" value="<?=e(number_format((float)($item['unit_price']??0),2,'.',''))?>" data-pos-price><?php else:?><input type="number" name="item_unit_price[]" min="0" step="0.01" value="<?=e(number_format((float)($item['unit_price']??0),2,'.',''))?>" data-pos-price><?php endif;?></td><td><strong data-pos-line-total>₱<?=number_format((float)($item['gross_amount']??((float)($item['quantity']??1)*(float)($item['unit_price']??0))),2)?></strong></td><td><?php if(!$clinical&&!$selectedChargeLocked):?><button type="button" class="pos-remove-item" data-pos-remove aria-label="Remove item">×</button><?php endif;?></td></tr>
        <?php endforeach;?></tbody></table></div>
        <div class="pos-no-items" data-pos-empty <?=$selectedItems?'hidden':''?>>No payable line has been loaded. Select an open invoice balance or a finalized visit above.</div>
      </div>

      <?php if(!$selectedChargeLocked && !$selectedBalanceOnly):?><details class="pos-additional-charges"<?=(!$selectedItems&&$retailProducts)?' open':''?>><summary>Add clinic service or retail product</summary><div class="pos-catalog-section"><p class="muted">Retail prices come from the protected product catalogue. Stock is deducted only after full payment succeeds. Clinical procedures must still come from the Dentist-finalized handoff.</p><?php if($retailProducts):?><h4>Products for sale</h4><div class="pos-catalog-grid"><?php foreach($retailProducts as $product):?><button type="button" class="pos-catalog-button retail" data-pos-add-item data-source-type="retail_product" data-description="<?=e($product['name'])?>" data-price="<?=e(number_format((float)$product['selling_price'],2,'.',''))?>" data-code="RET-<?=(int)$product['id']?>" data-inventory-id="<?=(int)$product['id']?>"><span><?=e($product['name'])?><small><?=e((string)$product['current_stock'])?> <?=e((string)$product['unit'])?> available</small></span><b>₱<?=number_format((float)$product['selling_price'],2)?></b></button><?php endforeach;?></div><?php else:?><div class="alert info">No active retail product currently has available stock. Dental Assistants can add catalogue items; Inventory or Administration must receive stock before they appear here.</div><?php endif;?><?php if($services):?><h4>Clinic services</h4><div class="pos-catalog-grid"><?php foreach($services as $service):?><button type="button" class="pos-catalog-button" data-pos-add-item data-source-type="service_catalog" data-description="<?=e($service['name'])?>" data-price="<?=e(number_format((float)$service['standard_fee'],2,'.',''))?>" data-code="SVC-<?=(int)$service['id']?>" data-service-id="<?=(int)$service['id']?>"><span><?=e($service['name'])?></span><b>₱<?=number_format((float)$service['standard_fee'],2)?></b></button><?php endforeach;?></div><?php endif;?><?php if($isSupervisor):?><div class="pos-custom-item"><input type="text" data-pos-custom-description placeholder="Administrator-authorized manual charge"><input type="number" min="0.01" step="0.01" data-pos-custom-price placeholder="Price"><button type="button" class="btn ghost" data-pos-add-custom>Add manual charge</button></div><?php endif;?></div></details><?php endif;?>
    </section>

    <aside class="pos-payment-panel"><div class="pos-panel-head"><div><strong><?=$selectedBalanceOnly?'Receive balance payment':'Receive payment'?></strong><span><?=$selectedBalanceOnly?'No checkup required; payment applies to this existing payable.':'Patient or another verified payee may provide payment'?></span></div><span class="pos-lock-indicator"><?=$selectedBalanceOnly?'Balance only':($selectedChargeLocked?'Charges locked':'Verified charges')?></span></div>
      <dl class="pos-totals"><dt>Subtotal</dt><dd data-pos-subtotal>₱0.00</dd><dt>Approved discount</dt><dd data-pos-discount-amount>− ₱0.00</dd><dt class="pos-grand-label">Total</dt><dd class="pos-grand-total" data-pos-total>₱0.00</dd><dt>Already paid</dt><dd data-pos-paid data-value="<?=e(number_format((float)($selectedInvoice['paid_amount']??0),2,'.',''))?>">₱<?=number_format((float)($selectedInvoice['paid_amount']??0),2)?></dd><dt>Balance</dt><dd class="pos-balance" data-pos-balance>₱0.00</dd></dl>
      <div class="pos-discount-box"><label class="wide">Approved discount<select name="discount_authorization_id" data-pos-discount-authorization <?=$selectedChargeLocked?'disabled':''?>><?php if($selectedChargeLocked && (float)($selectedInvoice['discount_amount']??0)>0):?><option value="0" data-type="<?=e($selectedInvoice['discount_type'])?>" data-value="<?=e($selectedInvoice['discount_value'])?>"><?=e($selectedInvoice['discount_reason']?:'Approved discount')?> · ₱<?=number_format((float)$selectedInvoice['discount_amount'],2)?></option><?php else:?><option value="0" data-type="none" data-value="0">No discount</option><?php foreach($approvedDiscounts as $row):?><option value="<?=(int)$row['id']?>" data-type="<?=e($row['discount_type'])?>" data-value="<?=e($row['discount_value'])?>"><?=e($row['reason'])?> · <?=e($row['discount_type']==='percent'?$row['discount_value'].'%':'₱'.number_format((float)$row['discount_value'],2))?></option><?php endforeach;?><?php endif;?></select></label><input type="hidden" data-pos-discount-type value="<?=e($selectedInvoice['discount_type']??'none')?>"><input type="hidden" data-pos-discount-value value="<?=e($selectedInvoice['discount_value']??0)?>"></div>
      <div class="pos-payee-box"><label>Paid by<input name="payer_name" value="<?=e($defaultPayerName)?>" required autocomplete="name"></label><label>Relationship<select name="payer_relationship"><option>Self</option><option>Parent</option><option>Guardian</option><option>Spouse</option><option>Relative</option><option>Employer</option><option>Other</option></select></label></div>
      <div class="pos-payment-method"><label>Payment method<select name="method" data-pos-method><option value="cash">Cash</option><option value="card">Card</option><option value="gcash">GCash</option><option value="maya">Maya</option><option value="bank_transfer">Bank transfer</option><option value="e-wallet">Other e-wallet</option><option value="cheque">Cheque</option><option value="mixed">Mixed payment</option><option value="other">Other</option></select></label><label>Amount tendered / paid<input type="number" name="tendered_amount" min="0" step="0.01" data-pos-tendered></label><label>External transaction / reference code<input name="reference_no" data-pos-reference minlength="4" maxlength="190" placeholder="Required for card, GCash, Maya, bank, cheque, mixed, and digital payments"></label><label>Payment note<textarea name="payment_notes" rows="2"></textarea></label></div>
      <div class="pos-change-card"><span>Change due</span><strong data-pos-change>₱0.00</strong><small data-pos-after-payment>Remaining balance ₱0.00</small></div>
      <div class="pos-checkout-actions"><button class="btn ghost" type="submit" name="checkout_mode" value="save" data-pos-save-only <?=(!$posReady||!$transactionActive||$selectedBalanceOnly)?'disabled':''?>>Save invoice only</button><button class="btn primary" type="submit" name="checkout_mode" value="pay" <?=(!$posReady||!$selectedQueueCalled||!$activeSession||!$transactionActive||(int)$selectedRegisterPreview['missing_fee_count']>0)?'disabled':''?> title="<?=(int)$selectedRegisterPreview['missing_fee_count']>0?'A completed procedure is missing its approved amount':(!($activeSession)?'Open a cashier session first':($selectedQueueCalled?($selectedBalanceOnly?'Receive balance payment without checkup':'Record payment'):'Call the patient at the cashier first'))?>"><?=$selectedBalanceOnly?'1. Record balance payment':'1. Record payment'?></button></div>
      <?php if($selectedQueue):?><section class="pos-next-action"><span class="eyebrow">Next required action</span><?php if($selectedQueueCompletionMode==='billing_correction'):?><h3>Return the visit for billing correction</h3><p>A completed procedure is missing an approved service or fee. Cashier must not invent the amount.</p><label>Correction reason<textarea name="correction_reason" form="cashier-correction-form" minlength="10" required placeholder="Completed procedure has no approved service or fee snapshot"></textarea></label><button class="btn danger" type="submit" form="cashier-correction-form" data-confirm="Return this visit to the responsible Dentist and release the focused cashier transaction?">Return to Dentist</button><?php elseif($selectedQueueCompletionMode==='blocked'):?><h3>Record the remaining payment first</h3><p>Patient responsibility remains ₱<?=number_format($selectedQueuePatientResponsibility,2)?>. The checkout button will become available after the payable balance or approved insurer responsibility is resolved.</p><button class="btn primary" type="button" disabled>Finalize checkout unavailable</button><?php else:?><h3><?= $selectedQueueCompletionMode==='insurance'?'Finalize insured checkout':($selectedQueueCompletionMode==='zero_charge'?'Finalize zero-balance checkout':'Complete patient checkout') ?></h3><?php if($selectedQueueCompletionMode==='zero_charge'):?><p>No amount is due. Record the legitimate reason, then close the visit.</p><label>Zero-balance reason<textarea name="zero_balance_reason" form="cashier-finalize-form" minlength="5" required placeholder="Included consultation, post-operative review, or no billable procedure"></textarea></label><?php elseif($selectedQueueCompletionMode==='insurance'):?><p>Approved insurer receivable ₱<?=number_format($selectedQueueInsuranceReceivable,2)?> · patient responsibility ₱<?=number_format($selectedQueuePatientResponsibility,2)?>.</p><?php else:?><p>All queue-linked balances are settled. Close the visit and release the register for the next patient.</p><?php endif;?><button class="btn primary" type="submit" form="cashier-finalize-form" <?=($selectedQueueCalled&&$transactionActive)?'':'disabled'?>><?= $selectedQueueCompletionMode==='zero_charge'?'Finalize zero-balance visit':'Finalize checkout and complete visit' ?></button><?php endif;?></section><?php endif;?>
      <?php if($selectedInvoice):?><div class="pos-print-actions"><a target="_blank" href="print_record.php?type=invoice&id=<?=(int)$selectedInvoice['id']?>">Print invoice</a><?php if($selectedReceiptPaymentId>0):?><a target="_blank" href="print_record.php?type=receipt&id=<?=(int)$selectedInvoice['id']?>&amp;payment_id=<?=$selectedReceiptPaymentId?>">Print latest payment receipt</a><?php endif;?></div><?php endif;?>
    </aside>
  </form>
</div>
<?php else:?><section class="pos-register-empty"><div><?=dental_icon('receipt','empty-icon')?><h2>Select a patient or payee account</h2><p>Use All Payables for balance-only payments, or select a patient from today’s cashier queue for visit checkout. No checkup is created for existing balance payments.</p></div></section><?php endif;?>
</div>

<?php if($selectedPatient&&$isSupervisor):?><details class="card cashier-supervisor-controls"><summary><span><strong>Supervisor exceptions and authorizations</strong><small>Discount requests and controlled charge-waiver review only. Routine payment and final checkout remain above.</small></span><span class="badge">Supervisor</span></summary><div class="module-card-grid"><article class="module-card"><h3>Request discount</h3><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="request_discount"><input type="hidden" name="patient_id" value="<?=$selectedPatientId?>"><input type="hidden" name="queue_entry_id" value="<?=(int)($selectedQueue['id']??0)?>"><label>Type<select name="discount_type"><option value="percent">Percent</option><option value="fixed">Fixed amount</option></select></label><label>Value<input type="number" min="0.01" step="0.01" name="discount_value" required></label><label class="wide">Reason / policy basis<textarea name="reason" required></textarea></label><label class="wide">Evidence reference<input name="evidence_reference"></label><button class="btn primary" <?= $transactionActive?'':'disabled' ?>>Submit request</button></form></article><?php if($selectedQueue&&$selectedQueueCompletionMode==='blocked'):?><article class="module-card"><h3>Request supervised charge-waiver review</h3><p class="muted">Use only when an actual approved charge may be waived under clinic policy.</p><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="request_zero_charge"><input type="hidden" name="patient_id" value="<?=$selectedPatientId?>"><input type="hidden" name="queue_entry_id" value="<?=(int)$selectedQueue['id']?>"><label>Category<select name="category"><?php foreach(['no_charge_followup','warranty_rework','package_included','staff_benefit','charity','waived_balance','other'] as $v):?><option value="<?=e($v)?>"><?=e(module_status($v))?></option><?php endforeach;?></select></label><label class="wide">Reason<textarea name="reason" required></textarea></label><label class="wide">Evidence reference<input name="evidence_reference"></label><button class="btn primary" <?= $transactionActive?'':'disabled' ?>>Submit request</button></form></article><?php endif;?></div></details><?php endif;?>


<?php if(in_array(active_role(), ['cashier','staff'], true)): ?>
<details class="card cashier-own-report compact-operator-report"><summary><span><strong>My payment and drawer summary</strong><small>Own payments, session status, and pending return requests.</small></span><span class="badge">Report</span></summary><?php if($cashierRoleReportError):?><div class="alert warning"><strong>Report temporarily unavailable.</strong> The cashier transaction area remains usable. Ask Superuser to apply pending database updates if this continues.</div><?php endif;?><section class="stats mini-stats"><?php stat_card('Payments today', $cashierRoleReport['payments_today'] ?? 0, 'Transactions received by you'); stat_card('Amount received', '₱'.number_format((float)($cashierRoleReport['amount_today'] ?? 0),2), 'Gross posted payments'); stat_card('Pending returns', $cashierRoleReport['pending_returns'] ?? 0, 'Requested or approved'); stat_card('Recent sessions', count($cashierRoleReport['session_rows'] ?? []), 'Latest drawer sessions'); ?></section><?php if(!empty($cashierRoleReport['session_rows'])):?><div class="table-wrap"><table><thead><tr><th>Opened</th><th>Closed</th><th>Status</th><th>Opening</th><th>Expected</th><th>Counted</th><th>Variance</th></tr></thead><tbody><?php foreach($cashierRoleReport['session_rows'] as $row): $opened = !empty($row['opened_at']) ? date('M j, g:i A', strtotime((string)$row['opened_at'])) : '—'; $closed = !empty($row['closed_at']) ? date('M j, g:i A', strtotime((string)$row['closed_at'])) : '—'; ?><tr><td><?=e($opened)?></td><td><?=e($closed)?></td><td><?=e(module_status((string)($row['status'] ?? 'open')))?></td><td>₱<?=number_format((float)($row['opening_cash'] ?? 0),2)?></td><td><?=array_key_exists('expected_cash',$row)&&$row['expected_cash']!==null?'₱'.number_format((float)$row['expected_cash'],2):'—'?></td><td><?=array_key_exists('counted_cash',$row)&&$row['counted_cash']!==null?'₱'.number_format((float)$row['counted_cash'],2):'—'?></td><td><?=array_key_exists('variance',$row)&&$row['variance']!==null?'₱'.number_format((float)$row['variance'],2):'—'?></td></tr><?php endforeach;?></tbody></table></div><?php endif;?></details>
<?php endif; ?>

<?php if($selectedInvoice && $selectedInvoicePayments):?>
<section class="card pos-receipt-history">
  <div class="card-head"><div><h2>Payment receipts</h2><span class="muted">Each payment has its own itemized receipt. Partial payments show only the items and amounts covered by that transaction.</span></div><?php if(function_exists('app_orientation_mode_active')&&app_orientation_mode_active()):?><span class="badge warning">DEMO COPY</span><?php endif;?></div>
  <div class="table-wrap"><table><thead><tr><th>Receipt</th><th>Date</th><th>Method</th><th>Reference</th><th>Amount</th><th>Actions</th></tr></thead><tbody>
  <?php foreach($selectedInvoicePayments as $receiptPayment):$receiptNo=(string)$selectedInvoice['invoice_no'].'-P'.str_pad((string)$receiptPayment['id'],4,'0',STR_PAD_LEFT);?>
    <tr><td><strong><?=e($receiptNo)?></strong></td><td><?=!empty($receiptPayment['created_at'])?e(date('M j, Y g:i A',strtotime((string)$receiptPayment['created_at']))):'—'?></td><td><?=e(module_status((string)$receiptPayment['method']))?></td><td><?=e((string)($receiptPayment['reference_no']?:'—'))?></td><td>₱<?=number_format((float)$receiptPayment['amount'],2)?></td><td><div class="inline-actions"><a target="_blank" href="print_record.php?type=receipt&amp;id=<?=(int)$selectedInvoice['id']?>&amp;payment_id=<?=(int)$receiptPayment['id']?>">Print itemized receipt</a><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="resend_receipt"><input type="hidden" name="payment_id" value="<?=(int)$receiptPayment['id']?>"><button class="btn ghost" type="submit">Email again</button></form></div></td></tr>
  <?php endforeach;?>
  </tbody></table></div>
</section>
<?php endif;?>

<?php if($checkoutRows):?><section class="card"><div class="card-head"><div><h2><?= $isSupervisor?'Cashier checkout activity':'My cashier checkout activity' ?></h2><span class="muted">Every completed visit is separated by cashier account, session, and physical window.</span></div></div><div class="table-wrap"><table><thead><tr><th>Completed</th><th>Patient / queue</th><th>Checkout</th><th>Cashier</th><th>Window</th><th>Reason / amount</th></tr></thead><tbody><?php foreach($checkoutRows as $checkout):?><tr><td><?=e(date('M j, Y g:i A',strtotime((string)$checkout['completed_at'])))?></td><td><strong><?=e((string)$checkout['patient'])?></strong><small><?=e((string)$checkout['patient_no'])?><?=!empty($checkout['queue_no'])?' · '.e((string)$checkout['queue_no']):''?></small></td><td><span class="badge"><?=e(module_status((string)$checkout['checkout_type']))?></span><small>Session #<?=(int)$checkout['cashier_session_id']?></small></td><td><?=e((string)$checkout['cashier_name'])?></td><td><?=e((string)$checkout['window_name'])?></td><td>₱<?=number_format((float)$checkout['amount_collected'],2)?><?php if(!empty($checkout['reason'])):?><small><?=e((string)$checkout['reason'])?></small><?php endif;?></td></tr><?php endforeach;?></tbody></table></div></section><?php endif;?>

<section class="card pos-recent-transactions" id="all-payables"><div class="card-head"><div><h2>All payables</h2><span class="muted">Cashier payment list only. Search patient/payee accounts, invoices, references, and balances from here.</span></div><form method="get" class="inline-form payable-filter-form"><?php foreach(['queue_entry_id','patient_id','invoice_id','encounter_id'] as $keep):if(!empty($_GET[$keep])):?><input type="hidden" name="<?=e($keep)?>" value="<?=e((string)$_GET[$keep])?>"><?php endif;endforeach;?><label>Search<input type="search" name="payable_q" value="<?=e($payableQuery)?>" placeholder="Patient, invoice, receipt reference, mobile"></label><label>Status<select name="payable_status" onchange="this.form.submit()"><option value="open" <?=$payableStatus==='open'?'selected':''?>>Open payables</option><option value="all" <?=$payableStatus==='all'?'selected':''?>>All non-voided</option><option value="paid" <?=$payableStatus==='paid'?'selected':''?>>Paid</option></select></label><label>Rows<select name="tx_per_page" onchange="this.form.submit()"><?php foreach([10,25,50,100] as $size):?><option value="<?=$size?>" <?=$txPerPage===$size?'selected':''?>><?=$size?></option><?php endforeach;?></select></label><button class="btn primary">Apply</button><a class="btn ghost" href="billing.php#all-payables">Reset</a></form></div><?php module_render_list_count($invoiceRowsTotal,'payables');module_render_server_pagination($invoiceRowsTotal,$txPage,$txPerPage,'tx_page','tx_per_page','recent-transactions');?><div class="table-wrap"><table><thead><tr><th>Invoice</th><th>Patient</th><th>Total</th><th>Paid / balance</th><th>Status</th><th>Latest payment</th><th>Actions</th></tr></thead><tbody>
<?php foreach($invoiceRows as $row):$balance=max(0,(float)$row['amount']-(float)$row['paid_amount']);$latestAvailable=max(0,(float)$row['latest_payment_amount']-(float)($row['latest_payment_adjusted']??0));?><tr><td><strong><?=e((string)$row['invoice_no'])?></strong><small><?=e(date('M j, Y g:i A',strtotime((string)$row['created_at'])))?></small></td><td><strong><?=e((string)$row['patient'])?></strong><small><?=e((string)$row['patient_no'])?><?=!empty($row['queue_no'])?' · '.e((string)$row['queue_no']):''?></small></td><td>₱<?=number_format((float)$row['amount'],2)?></td><td>₱<?=number_format((float)$row['paid_amount'],2)?><small>Balance ₱<?=number_format($balance,2)?></small></td><td><span class="badge"><?=e(module_status((string)$row['status']))?></span></td><td><?php if((int)$row['latest_payment_id']>0):?>₱<?=number_format($latestAvailable,2)?> net<small>Gross ₱<?=number_format((float)$row['latest_payment_amount'],2)?> · Returned ₱<?=number_format((float)($row['latest_payment_adjusted']??0),2)?><br><?=e(module_status((string)($row['latest_payment_source']??'patient')))?> · <?=e(module_status((string)$row['latest_payment_method']))?></small><?php else:?><span class="muted">No payment</span><?php endif;?></td><td><div class="inline-actions"><a href="billing.php?invoice_id=<?=(int)$row['id']?>&amp;patient_id=<?=(int)$row['patient_id']?><?= $balance>0.001?'&amp;balance_only=1':(!empty($row['queue_entry_id'])?'&amp;queue_entry_id='.(int)$row['queue_entry_id']:'') ?>"><?= $balance>0.001?'Pay balance':'Open POS' ?></a><a target="_blank" href="print_record.php?type=invoice&amp;id=<?=(int)$row['id']?>">Invoice</a><a target="_blank" href="print_record.php?type=patient_summary&amp;document=ledger&amp;id=<?=(int)$row['patient_id']?>">Statement</a><?php if((int)$row['latest_payment_id']>0):?><a target="_blank" href="print_record.php?type=receipt&amp;id=<?=(int)$row['id']?>&amp;payment_id=<?=(int)$row['latest_payment_id']?>">Latest receipt</a><?php endif;?><?php if((int)($row['latest_adjustment_id']??0)>0):?><a target="_blank" href="print_record.php?type=payment_adjustment&amp;id=<?=(int)$row['latest_adjustment_id']?>">Return receipt</a><?php endif;?><?php if($isSupervisor&&(int)$row['latest_payment_id']>0&&$latestAvailable>0.001&&(string)($row['latest_payment_source']??'patient')==='patient'):?><a class="btn ghost" href="<?=e(module_management_url(['return_payment_id'=>(int)$row['latest_payment_id'],'tx_page'=>$txPage,'tx_per_page'=>$txPerPage]))?>">Request return</a><?php endif;?></div></td></tr><?php endforeach;?><?php if(!$invoiceRows):?><tr><td colspan="7" class="muted">No matching payables or transactions were found.</td></tr><?php endif;?></tbody></table></div><?php module_render_server_pagination($invoiceRowsTotal,$txPage,$txPerPage,'tx_page','tx_per_page','recent-transactions');?></section>
<?php if($managedReturnInvoice&&$isSupervisor):$managedAvailable=max(0,(float)$managedReturnInvoice['latest_payment_amount']-(float)($managedReturnInvoice['latest_payment_adjusted']??0));?><dialog id="return-request" class="wide-dialog" data-auto-open-dialog="return-request"><form method="dialog"><button type="submit" class="dialog-close">×</button></form><div class="dialog-scroll"><h2>Request refund or reversal</h2><p><strong><?=e((string)$managedReturnInvoice['invoice_no'])?> · <?=e((string)$managedReturnInvoice['patient'])?></strong></p><div class="detail-grid"><div><small>Gross payment</small><strong>₱<?=number_format((float)$managedReturnInvoice['latest_payment_amount'],2)?></strong></div><div><small>Already returned</small><strong>₱<?=number_format((float)($managedReturnInvoice['latest_payment_adjusted']??0),2)?></strong></div><div><small>Available</small><strong>₱<?=number_format($managedAvailable,2)?></strong></div><div><small>Original method</small><strong><?=e(module_status((string)$managedReturnInvoice['latest_payment_method']))?></strong></div></div><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="request_adjustment"><input type="hidden" name="payment_id" value="<?=(int)$managedReturnInvoice['latest_payment_id']?>"><input type="hidden" name="method" value="<?=e((string)$managedReturnInvoice['latest_payment_method'])?>"><label>Type<select name="adjustment_type"><option value="refund">Refund — partial or full</option><option value="reversal">Reversal — full remaining payment only</option></select></label><label>Amount<input type="number" min="0.01" max="<?=e(number_format($managedAvailable,2,'.',''))?>" step="0.01" name="amount" required></label><label>Processor reference<input name="reference_no" <?=((string)$managedReturnInvoice['latest_payment_method']!=='cash')?'required':''?>></label><label class="wide">Reason<textarea name="reason" required></textarea></label><button class="btn primary">Submit for approval</button></form></div></dialog><?php endif;?>
<?php render_footer(); ?>
