<?php
declare(strict_types=1);

function app_config(?string $key = null, mixed $default = null): mixed
{
    static $config = null;
    if ($config === null) {
        $config = require __DIR__ . '/config.php';
    }

    if ($key === null) {
        return $config;
    }

    return $config[$key] ?? $default;
}

function app_root(string $path = ''): string
{
    $root = dirname(__DIR__);
    if ($path === '') {
        return $root;
    }

    return $root . DIRECTORY_SEPARATOR . ltrim($path, '\\/');
}

function app_public_path(string $path = ''): string
{
    return app_root('public' . ($path !== '' ? DIRECTORY_SEPARATOR . ltrim($path, '\\/') : ''));
}

function app_storage_path(string $path = ''): string
{
    return app_root('storage' . ($path !== '' ? DIRECTORY_SEPARATOR . ltrim($path, '\\/') : ''));
}

function app_ensure_dir(string $path): void
{
    if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) {
        throw new RuntimeException('Unable to create directory: ' . $path);
    }
}

function app_escape(mixed $value): string
{
    return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function app_i18n_map(): array
{
    return [
        'ORION ACCOUNTS ERP' => 'أوريون للحسابات ERP',
        'Unified Accounting, Finance, Inventory, and Payroll Platform' => 'منصة موحدة للحسابات والمالية والمخزون والرواتب',
        'Dashboard' => 'لوحة المؤشرات',
        'Report Designer' => 'مصمم التقارير',
        'Accounting Reports' => 'التقارير المحاسبية',
        'Reports' => 'التقارير',
        'Accounting Report' => 'تقرير محاسبي',
        'Accounting Reports Menu' => 'قائمة التقارير المحاسبية',
        'Modules' => 'الوحدات',
        'Logout' => 'تسجيل الخروج',
        'Login' => 'دخول',
        'Sign In' => 'تسجيل الدخول',
        'Username' => 'اسم المستخدم',
        'Password' => 'كلمة المرور',
        'Use your account to access modules, imports, and report designer.' => 'استخدم حسابك للوصول إلى الوحدات والاستيراد ومصمم التقارير.',
        'Secure accounting workspace' => 'مساحة عمل محاسبية آمنة',
        'Multi-company and role-based access' => 'تعدد الشركات والصلاحيات حسب الدور',
        'Please enter username and password.' => 'من فضلك أدخل اسم المستخدم وكلمة المرور.',
        'Invalid login credentials.' => 'بيانات الدخول غير صحيحة.',
        'Database bootstrap error:' => 'خطأ تجهيز قاعدة البيانات:',
        'System bootstrap failed' => 'فشل تشغيل النظام',
        'Bootstrap Error' => 'خطأ التشغيل',
        'The app could not connect to MySQL or run migrations.' => 'تعذر اتصال النظام بقاعدة MySQL أو تنفيذ التحديثات.',
        'Date :' => 'التاريخ :',
        'Company :' => 'الشركة :',
        'Module :' => 'الوحدة :',
        'Page :' => 'الصفحة :',
        'Prepared by :' => 'إعداد :',
        'Back' => 'رجوع',
        'Print' => 'طباعة',
        'Context' => 'السياق',
        'Choose company and dashboard template' => 'اختر الشركة وقالب لوحة المؤشرات',
        'Company' => 'الشركة',
        'Report Template' => 'قالب التقرير',
        'No template' => 'بدون قالب',
        'Total Records' => 'إجمالي السجلات',
        'Across all active modules' => 'على مستوى جميع الوحدات النشطة',
        'Users' => 'المستخدمون',
        'Active system users' => 'مستخدمو النظام النشطون',
        'Companies' => 'الشركات',
        'Managed entities' => 'الكيانات المدارة',
        'Templates' => 'القوالب',
        'Saved report designs' => 'تصميمات تقارير محفوظة',
        'Designed Report Output' => 'مخرجات التقرير المصمم',
        'No template selected' => 'لم يتم اختيار قالب',
        'No template available yet. Create one from the Report Designer page.' => 'لا يوجد قالب حتى الآن. أنشئ قالبا من صفحة مصمم التقارير.',
        'Template exists but has no sections. Edit template configuration in Report Designer.' => 'القالب موجود لكن بلا بنود. عدل إعداداته من مصمم التقارير.',
        'Module Activity' => 'نشاط الوحدات',
        'Quick totals per module' => 'إجماليات سريعة لكل وحدة',
        'Module' => 'الوحدة',
        'Description' => 'البيان',
        'Total Rows' => 'إجمالي الصفوف',
        'Open' => 'فتح',
        'Full accounting suite with CRUD modules, Excel import, and report designer.' => 'منظومة محاسبية متكاملة تشمل الإدخال والمراجعة والاستيراد من Excel وتصميم التقارير.',
        'Company Context' => 'سياق الشركة',
        'Select company before designing reports' => 'اختر الشركة قبل تصميم التقارير',
        'Create Template' => 'إنشاء قالب',
        'Edit Template' => 'تعديل القالب',
        'Add multiple sections and comparison columns per section' => 'أضف بنودا متعددة وأعمدة مقارنة لكل بند',
        'Template Name' => 'اسم القالب',
        'Scope' => 'النطاق',
        'Default Chart Type' => 'نوع الرسم الافتراضي',
        'Active template' => 'قالب نشط',
        'Template Sections' => 'بنود القالب',
        'Each section can compare one item against many metrics' => 'كل بند يمكنه مقارنة عنصر واحد بعدة مؤشرات',
        'Add Section' => 'إضافة بند',
        'Save Template' => 'حفظ القالب',
        'New Template' => 'قالب جديد',
        'Saved Templates' => 'القوالب المحفوظة',
        'No templates yet for this company.' => 'لا توجد قوالب لهذه الشركة بعد.',
        'Name' => 'الاسم',
        'Default Chart' => 'الرسم الافتراضي',
        'Status' => 'الحالة',
        'Created' => 'تاريخ الإنشاء',
        'Actions' => 'الإجراءات',
        'Edit' => 'تعديل',
        'Delete' => 'حذف',
        'Delete this template?' => 'حذف هذا القالب؟',
        'Report Designer for section-based multi-metric comparisons.' => 'مصمم تقارير للمقارنات متعددة المؤشرات حسب البنود.',
        'Section' => 'بند',
        'Define one item and multiple comparison columns' => 'حدد عنصرا واحدا وعدة أعمدة مقارنة',
        'Section Title' => 'عنوان البند',
        'Label Column' => 'عمود التسمية',
        'Chart Type' => 'نوع الرسم',
        'Section Style' => 'شكل البند',
        'Top N Rows' => 'أعلى عدد صفوف',
        'Sort Mode' => 'طريقة الترتيب',
        'Date Column (Optional)' => 'عمود التاريخ (اختياري)',
        'Date From (Optional)' => 'من تاريخ (اختياري)',
        'Date To (Optional)' => 'إلى تاريخ (اختياري)',
        'Comparison Columns (choose one or more)' => 'أعمدة المقارنة (اختر واحدا أو أكثر)',
        'Use Ctrl/Cmd click to select multiple metrics for this section.' => 'استخدم Ctrl/Cmd لاختيار أكثر من مؤشر لهذا البند.',
        'Remove Section' => 'حذف البند',
        'Classic' => 'كلاسيك',
        'Glass' => 'زجاجي',
        'Sunset' => 'غروب',
        'Forest' => 'أخضر',
        'Royal' => 'ملكي',
        'Bar' => 'أعمدة',
        'Line' => 'خطي',
        'Pie' => 'دائري',
        'Doughnut' => 'حلقي',
        'Label A-Z' => 'التسمية تصاعدي',
        'Top by first metric' => 'الأعلى حسب أول مؤشر',
        'Any date column' => 'أي عمود تاريخ',
        'Report Filters' => 'فلاتر التقرير',
        'Select company, period, and account' => 'اختر الشركة والفترة والحساب',
        'From Date' => 'من تاريخ',
        'To Date' => 'إلى تاريخ',
        'Ledger Account' => 'حساب الأستاذ',
        'Choose account for statement' => 'اختر حساب كشف الحساب',
        'Cost Center' => 'مركز التكلفة',
        'All cost centers' => 'كل مراكز التكلفة',
        'Used by Cost Center report when selected.' => 'يستخدم في تقرير مركز التكلفة عند الاختيار.',
        'Include zero-balance accounts' => 'إظهار الحسابات ذات الرصيد الصفري',
        'Apply' => 'تطبيق',
        'Reset' => 'إعادة ضبط',
        'Search' => 'بحث',
        'Clear' => 'مسح',
        'Account Statement' => 'كشف الحساب',
        'Account Statement (Ledger)' => 'كشف الحساب (الأستاذ)',
        'Journal Book' => 'دفتر اليومية',
        'Cost Center Report' => 'تقرير مراكز التكلفة',
        'Contact Statement' => 'كشف حساب جهة الاتصال',
        'VAT Summary' => 'ملخص ض.ق.م',
        'Trial Balance' => 'ميزان المراجعة',
        'Income Statement' => 'قائمة الدخل',
        'Balance Sheet' => 'قائمة المركز المالي',
        'Statement of Financial Position' => 'قائمة المركز المالي',
        'Financial Statement Groups' => 'بنود القوائم المالية',
        'Financial Statement Group' => 'بند القوائم المالية',
        'Financial Statement Group Code' => 'كود بند القوائم المالية',
        'Financial Statement Group Name' => 'اسم بند القوائم المالية',
        'Choose Financial Statement Group' => 'اختر بند القوائم المالية',
        'Assign Group' => 'ربط بالبند',
        'Assign selected accounts to this financial statement group?' => 'ربط الحسابات المحددة ببند القوائم المالية؟',
        'Statement Type' => 'نوع القائمة',
        'Main Section' => 'البند الرئيسي',
        'Sort Order' => 'ترتيب العرض',
        'Non-current Assets' => 'الأصول غير المتداولة',
        'Total Non-current Assets' => 'إجمالي الأصول غير المتداولة',
        'Current Assets' => 'الأصول المتداولة',
        'Total Current Assets' => 'إجمالي الأصول المتداولة',
        'Equity' => 'حقوق الملكية',
        'Total Equity' => 'إجمالي حقوق الملكية',
        'Non-current Liabilities' => 'الالتزامات غير المتداولة',
        'Total Non-current Liabilities' => 'إجمالي الالتزامات غير المتداولة',
        'Current Liabilities' => 'الالتزامات المتداولة',
        'Total Current Liabilities' => 'إجمالي الالتزامات المتداولة',
        'Revenue' => 'الإيرادات',
        'Total Revenue' => 'إجمالي الإيرادات',
        'Revenue Deductions' => 'يخصم من الإيرادات',
        'Total Revenue Deductions' => 'إجمالي الخصم من الإيرادات',
        'Cost of Revenue' => 'تكلفة الإيرادات',
        'Total Cost of Revenue' => 'إجمالي تكلفة الإيرادات',
        'Operating Expenses' => 'مصروفات تشغيلية وعمومية',
        'Total Operating Expenses' => 'إجمالي المصروفات التشغيلية والعمومية',
        'Other Income' => 'إيرادات أخرى',
        'Total Other Income' => 'إجمالي الإيرادات الأخرى',
        'Finance Costs' => 'تكاليف تمويلية',
        'Total Finance Costs' => 'إجمالي التكاليف التمويلية',
        'Income Tax' => 'ضريبة الدخل',
        'Total Income Tax' => 'إجمالي ضريبة الدخل',
        'Unmapped Accounts' => 'حسابات غير مربوطة ببند',
        'Cash Flow' => 'التدفقات النقدية',
        'Choose an account from filters above' => 'اختر حسابا من الفلاتر بالأعلى',
        'Choose contact for statement' => 'اختر جهة اتصال لعرض كشف الحساب',
        'Select a ledger account to display detailed account statement.' => 'اختر حسابا لعرض كشف الحساب التفصيلي.',
        'Select a contact to display detailed statement.' => 'اختر جهة اتصال لعرض كشف الحساب التفصيلي.',
        'No transactions for this contact in the selected period.' => 'لا توجد حركات لهذه الجهة في الفترة المحددة.',
        'No posted transactions for this account in the selected period.' => 'لا توجد حركات مرحلة لهذا الحساب في الفترة المحددة.',
        'Opening Balance' => 'الرصيد الافتتاحي',
        'Total Debit' => 'إجمالي المدين',
        'Total Credit' => 'إجمالي الدائن',
        'Closing Balance' => 'الرصيد الختامي',
        'Date' => 'التاريخ',
        'Account' => 'الحساب',
        'Line' => 'السطر',
        'Source' => 'المصدر',
        'Manual' => 'يدوي',
        'Period' => 'الفترة',
        'Label' => 'التسمية',
        'Metric' => 'المؤشر',
        'Series' => 'السلسلة',
        'Total' => 'الإجمالي',
        'Rows' => 'الصفوف',
        'Detailed Lines' => 'السطور التفصيلية',
        'Reference' => 'المرجع',
        'Debit' => 'مدين',
        'Credit' => 'دائن',
        'Balance' => 'الرصيد',
        'Detailed posted journal lines by account name, description, debit, and credit.' => 'سطور القيود المرحلة تفصيليا باسم الحساب والبيان والمدين والدائن.',
        'Fiscal Year' => 'السنة المالية',
        'Expense Account' => 'حساب المصروف',
        'Budget' => 'الموازنة',
        'Spent' => 'المنصرف',
        'Remaining' => 'المتبقي',
        'Run Automatic Depreciation' => 'تشغيل الاهلاك التلقائي',
        'Create monthly depreciation entries and post journal lines automatically.' => 'إنشاء قيود اهلاك شهرية وترحيلها تلقائيا.',
        'Cannot run depreciation yet. Please ensure Cost Centers, Expense accounts, and Asset accounts are configured.' => 'لا يمكن تشغيل الاهلاك الآن. تأكد من إعداد مراكز التكلفة وحسابات المصروف والأصول.',
        'Run Date' => 'تاريخ التشغيل',
        'Asset to Depreciate' => 'الأصل المراد إهلاكه',
        'All active assets' => 'كل الأصول النشطة',
        'Depreciation Start Date' => 'تاريخ بداية الإهلاك',
        'Depreciation Duration (Months)' => 'مدة الإهلاك بالشهور',
        'Automatic Depreciation' => 'إهلاك تلقائي',
        'Enable Auto Depreciation' => 'تفعيل الإهلاك التلقائي',
        'Accumulated Depreciation Account' => 'حساب مجمع الاهلاك',
        'No active cost centers' => 'لا توجد مراكز تكلفة نشطة',
        'No expense accounts' => 'لا توجد حسابات مصروفات',
        'No asset accounts' => 'لا توجد حسابات أصول',
        'Notes' => 'ملاحظات',
        'Run Depreciation' => 'تشغيل الاهلاك',
        'Clear Selection' => 'مسح الاختيار',
        'Select' => 'تحديد',
        'Select All' => 'تحديد الكل',
        'Delete Selected' => 'حذف المحدد',
        'Delete selected records?' => 'حذف السجلات المحددة؟',
        'No records selected.' => 'لم يتم تحديد أي سجلات.',
        'selected' => 'محدد',
        'You cannot delete the current logged-in user.' => 'لا يمكنك حذف المستخدم الحالي.',
        'Use workflow buttons to move this document between states.' => 'استخدم أزرار سير العمل لنقل المستند بين الحالات.',
        'Manual entry for this module.' => 'إدخال يدوي لهذه الوحدة.',
        'New' => 'جديد',
        'Save' => 'حفظ',
        'Update' => 'تحديث',
        'Import Excel/CSV' => 'استيراد Excel/CSV',
        'File' => 'الملف',
        'Supported: CSV and XLSX. First non-empty row is used as header.' => 'المدعوم: CSV و XLSX. أول صف غير فارغ يستخدم كعناوين.',
        'Expected Fields' => 'الحقول المتوقعة',
        'Start Import' => 'بدء الاستيراد',
        'Records' => 'السجلات',
        'No records found for this module and company context.' => 'لا توجد سجلات لهذه الوحدة ضمن سياق الشركة الحالي.',
        'View' => 'عرض',
        'Journal Entry' => 'قيد يومية',
        'Open Source' => 'فتح المصدر',
        'Delete this record?' => 'حذف هذا السجل؟',
        'Register Payment' => 'تسجيل دفعة',
        'Confirm' => 'اعتماد',
        'Cancel' => 'إلغاء',
        'Reset to Draft' => 'إرجاع لمسودة',
        'Post' => 'ترحيل',
        'Reverse' => 'عكس القيد',
        'Draft' => 'مسودة',
        'Posted' => 'مرحل',
        'Partially Paid' => 'مدفوع جزئيا',
        'Paid' => 'مدفوع',
        'Cancelled' => 'ملغي',
        'Approved' => 'معتمد',
        'Reversed' => 'معكوس',
        'Active' => 'نشط',
        'Inactive' => 'غير نشط',
        'Yes' => 'نعم',
        'No' => 'لا',
        'Choose' => 'اختر',
        'N/A' => 'غير متاح',
        'Admin' => 'مدير',
        'Accountant' => 'محاسب',
        'Auditor' => 'مراجع',
        'All Companies' => 'كل الشركات',
        'No Company' => 'بدون شركة',
        'Select Company' => 'اختيار الشركة',
        'Choose Company' => 'اختيار الشركة',
        'Switch Company' => 'تغيير الشركة',
        'Change Password' => 'تغيير كلمة المرور',
        'Current Password' => 'كلمة المرور الحالية',
        'New Password' => 'كلمة المرور الجديدة',
        'Confirm Password' => 'تأكيد كلمة المرور',
        'Save Password' => 'حفظ كلمة المرور',
        'Keep your account access secure.' => 'حافظ على أمان حسابك.',
        'Update the password for your current user only.' => 'تحديث كلمة مرور المستخدم الحالي فقط.',
        'Current Company' => 'الشركة الحالية',
        'Available Companies' => 'الشركات المتاحة',
        'Allowed Companies' => 'الشركات المسموح بها',
        'Primary Company' => 'الشركة الأساسية',
        'Continue' => 'متابعة',
        'Choose the company you want to work on.' => 'اختر الشركة التي تريد العمل عليها.',
        'Your account is assigned to more than one company.' => 'حسابك مرتبط بأكثر من شركة.',
        'You can switch company later from the sidebar.' => 'يمكنك تغيير الشركة لاحقا من القائمة الجانبية.',
        'No companies available for this user.' => 'لا توجد شركات متاحة لهذا المستخدم.',
        'Please select a company first.' => 'من فضلك اختر الشركة أولا.',
        'You do not have access to this company.' => 'ليس لديك صلاحية على هذه الشركة.',
        'Admins can access all active companies.' => 'المديرون يمكنهم الدخول إلى كل الشركات النشطة.',
        'Choose one or more companies this user can access.' => 'اختر شركة أو أكثر يمكن لهذا المستخدم الدخول إليها.',
        'Unassigned' => 'غير معين',
        'Dark mode' => 'الوضع الداكن',
        'Light mode' => 'الوضع الفاتح',
        'Language' => 'اللغة',
        'Arabic' => 'العربية',
        'English' => 'English',
        'Theme' => 'الثيم',
        'Collapse sidebar' => 'تصغير القائمة',
        'Expand sidebar' => 'توسيع القائمة',
        'Toggle sidebar' => 'تبديل القائمة',
        'Site Navigation' => 'تنقل الموقع',
        'Operations' => 'الحركة',
        'System Settings' => 'إعدادات النظام',
        'Companies' => 'الشركات',
        'Manage legal entities and branding.' => 'إدارة الكيانات القانونية والهوية.',
        'Legal Name' => 'الاسم القانوني',
        'Logo Path' => 'مسار الشعار',
        'Currency' => 'العملة',
        'Fiscal Start (MM-DD)' => 'بداية السنة المالية (MM-DD)',
        'Control access and role assignments.' => 'إدارة صلاحيات الدخول والأدوار.',
        'Full Name' => 'الاسم الكامل',
        'Role' => 'الدور',
        'Company ID' => 'معرف الشركة',
        'Chart of Accounts' => 'شجرة الحسابات',
        'Create the accounting tree and opening balances.' => 'إنشاء شجرة الحسابات والأرصدة الافتتاحية.',
        'Account Code' => 'كود الحساب',
        'Account Name' => 'اسم الحساب',
        'Type' => 'النوع',
        'Asset' => 'أصل',
        'Liability' => 'التزام',
        'Equity' => 'حقوق ملكية',
        'Revenue' => 'إيراد',
        'Expense' => 'مصروف',
        'Parent Code' => 'كود الحساب الأب',
        'Contacts' => 'جهات الاتصال',
        'Maintain customers, vendors, and balances.' => 'إدارة العملاء والموردين والأرصدة.',
        'Code' => 'الكود',
        'Tax Number' => 'الرقم الضريبي',
        'Phone' => 'الهاتف',
        'Email' => 'البريد الإلكتروني',
        'Address' => 'العنوان',
        'Products' => 'المنتجات',
        'Products catalog and stock balances.' => 'كتالوج المنتجات وأرصدة المخزون.',
        'Category' => 'التصنيف',
        'Category Code' => 'كود التصنيف',
        'Category Name' => 'اسم التصنيف',
        'Unit' => 'الوحدة',
        'Sale Price' => 'سعر البيع',
        'Purchase Price' => 'سعر الشراء',
        'Qty On Hand' => 'الكمية المتاحة',
        'Tax %' => 'نسبة الضريبة %',
        'Tax Rates' => 'معدلات الضريبة',
        'Tax definitions for sales and purchases.' => 'تعريفات الضريبة للمبيعات والمشتريات.',
        'Tax Name' => 'اسم الضريبة',
        'Rate %' => 'النسبة %',
        'Applies To' => 'تطبق على',
        'Sales' => 'مبيعات',
        'Purchase' => 'مشتريات',
        'Both' => 'كلاهما',
        'Bank Accounts' => 'الحسابات البنكية',
        'Manage treasury and bank balances.' => 'إدارة الخزينة وأرصدة البنوك.',
        'Bank Name' => 'اسم البنك',
        'GL Account' => 'حساب الأستاذ',
        'Account Number' => 'رقم الحساب',
        'Current Balance' => 'الرصيد الحالي',
        'Cost Centers' => 'مراكز التكلفة',
        'Track costs by department or business line.' => 'تتبع التكلفة حسب القسم أو النشاط.',
        'Projects' => 'المشروعات',
        'Project accounting and status tracking.' => 'محاسبة المشروعات وتتبع حالتها.',
        'Client' => 'العميل',
        'Start Date' => 'تاريخ البداية',
        'End Date' => 'تاريخ النهاية',
        'Budgets' => 'الموازنات',
        'Annual and departmental budgeting records.' => 'سجلات الموازنات السنوية والإدارية.',
        'Budget Name' => 'اسم الموازنة',
        'Amount' => 'المبلغ',
        'Journal Entries' => 'قيود اليومية',
        'Manual accounting entries and posting status.' => 'قيود محاسبية يدوية وحالة الترحيل.',
        'Entry Number' => 'رقم القيد',
        'Entry Date' => 'تاريخ القيد',
        'Sales Invoices' => 'فواتير المبيعات',
        'Billing, collections, and receivable tracking.' => 'الفوترة والتحصيل وتتبع العملاء.',
        'Invoice Number' => 'رقم الفاتورة',
        'Invoice Date' => 'تاريخ الفاتورة',
        'Customer' => 'العميل',
        'Revenue Account' => 'حساب الإيراد',
        'Taxable Invoice' => 'فاتورة خاضعة للضريبة',
        'Tax Code' => 'كود الضريبة',
        'Tax Rate %' => 'نسبة الضريبة %',
        'Subtotal' => 'الإجمالي قبل الضريبة',
        'Tax' => 'الضريبة',
        'Discount' => 'الخصم',
        'Grand Total' => 'الإجمالي النهائي',
        'Collected In Entry' => 'المحصل داخل الإدخال',
        'Collection Method' => 'طريقة التحصيل',
        'Collected In' => 'تم التحصيل في',
        'Purchase Bills' => 'فواتير المشتريات',
        'Vendor bills and payable tracking.' => 'فواتير الموردين وتتبع الدائنين.',
        'Purchase Returns' => 'مرتجع مشتريات',
        'Returns against vendor bills, with full or partial return lines.' => 'مرتجعات على فواتير الموردين ببنود كلية أو جزئية.',
        'Bill Number' => 'رقم فاتورة المورد',
        'Bill Date' => 'تاريخ الفاتورة',
        'Vendor' => 'المورد',
        'Taxable Bill' => 'فاتورة مشتريات خاضعة للضريبة',
        'Paid In Entry' => 'المدفوع داخل الإدخال',
        'Payment Method' => 'طريقة الدفع',
        'Paid From' => 'مدفوع من',
        'Payments' => 'المدفوعات',
        'Cash and bank transactions.' => 'حركات النقدية والبنوك.',
        'Sales Returns' => 'مرتجع مبيعات',
        'Returns against sales invoices, with full or partial return lines.' => 'مرتجعات على فواتير المبيعات ببنود كلية أو جزئية.',
        'Expense Returns' => 'مرتجع مصروفات',
        'Returns against direct expenses, with full or partial return lines.' => 'مرتجعات على المصروفات المباشرة ببنود كلية أو جزئية.',
        'Return Number' => 'رقم المرتجع',
        'Return Date' => 'تاريخ المرتجع',
        'Return Type' => 'نوع المرتجع',
        'Full Return' => 'مرتجع كلي',
        'Partial Return' => 'مرتجع جزئي',
        'Original Invoice' => 'الفاتورة الأصلية',
        'Original Bill' => 'فاتورة الشراء الأصلية',
        'Original Expense' => 'المصروف الأصلي',
        'Refund In Entry' => 'المبلغ المرتد داخل الإدخال',
        'Refund Method' => 'طريقة رد المبلغ',
        'Refund Account' => 'حساب رد المبلغ',
        'Payment Number' => 'رقم الدفع',
        'Payment Date' => 'تاريخ الدفع',
        'Direction' => 'الاتجاه',
        'Incoming' => 'وارد',
        'Outgoing' => 'صادر',
        'Method' => 'الطريقة',
        'Cash' => 'نقدي',
        'Bank' => 'بنك',
        'Card' => 'بطاقة',
        'Transfer' => 'تحويل',
        'Vendor Bill' => 'فاتورة مورد',
        'Contact' => 'جهة الاتصال',
        'Bank/Cash Account' => 'حساب البنك/الخزينة',
        'Offset Account (Optional)' => 'الحساب المقابل (اختياري)',
        'Fixed Assets' => 'الأصول الثابتة',
        'Assets register and depreciation fields.' => 'سجل الأصول وحقول الاهلاك.',
        'Assets' => 'الأصول',
        'Liabilities' => 'الالتزامات',
        'Revenues' => 'الإيرادات',
        'Expenses' => 'المصروفات',
        'Net Income' => 'صافي الدخل',
        'Total Revenue' => 'إجمالي الإيرادات',
        'Total Expenses' => 'إجمالي المصروفات',
        'Net Cash Movement' => 'صافي حركة النقدية',
        'Asset Code' => 'كود الأصل',
        'Asset Name' => 'اسم الأصل',
        'Purchase Date' => 'تاريخ الشراء',
        'Purchase Cost' => 'تكلفة الشراء',
        'Useful Life (Months)' => 'العمر الإنتاجي (بالشهور)',
        'Salvage Value' => 'القيمة التخريدية',
        'Straight Line' => 'القسط الثابت',
        'Declining' => 'المتناقص',
        'Units of Production' => 'وحدات الإنتاج',
        'Accumulated Depreciation' => 'مجمع الاهلاك',
        'Disposed' => 'مستبعد',
        'Under Maintenance' => 'تحت الصيانة',
        'Automatic Entries' => 'قيود تلقائية',
        'Run monthly fixed-asset depreciation and auto-post journal entries.' => 'تشغيل اهلاك الأصول الثابتة شهريا وترحيل القيود تلقائيا.',
        'Run Number' => 'رقم التشغيل',
        'Period Month' => 'شهر الفترة',
        'Period Year' => 'سنة الفترة',
        'Accumulated Account' => 'حساب المجمع',
        'Total Amount' => 'إجمالي المبلغ',
        'Assets Count' => 'عدد الأصول',
        'Employees' => 'الموظفون',
        'Employee master data for payroll.' => 'بيانات الموظفين الأساسية للرواتب.',
        'Employee No' => 'رقم الموظف',
        'Department' => 'القسم',
        'Basic Salary' => 'الراتب الأساسي',
        'Allowance' => 'البدل',
        'Default Deduction' => 'الاستقطاع الافتراضي',
        'Payroll Runs' => 'تشغيل الرواتب',
        'Payroll cycle totals and status.' => 'إجماليات دورة الرواتب وحالتها.',
        'Month' => 'الشهر',
        'Year' => 'السنة',
        'Total Gross' => 'إجمالي المستحق',
        'Total Deductions' => 'إجمالي الاستقطاعات',
        'Total Net' => 'صافي الراتب',
        'Inventory Movements' => 'حركات المخزون',
        'Stock in/out movements and valuation.' => 'حركات دخول وخروج المخزون والتقييم.',
        'Movement Number' => 'رقم الحركة',
        'Movement Date' => 'تاريخ الحركة',
        'Quantity' => 'الكمية',
        'Unit Cost' => 'تكلفة الوحدة',
        'Report Templates' => 'قوالب التقارير',
        'Saved report design templates and chart preferences.' => 'قوالب تصميم التقارير وإعدادات الرسوم.',
        'Module Scope' => 'نطاق الوحدة',
        'Config JSON' => 'إعدادات JSON',
    ];
}

function app_i18n_professional_map(): array
{
    return [
        // Professional wording overrides.
        'Modules' => 'الوحدات',
        'Module' => 'الوحدة',
        'Module :' => 'الوحدة :',
        'Module Activity' => 'نشاط الوحدات',
        'Quick totals per module' => 'إجماليات سريعة لكل وحدة',
        'Full accounting suite with CRUD modules, Excel import, and report designer.' => 'منظومة محاسبية متكاملة تشمل الإدخال والتعديل والاستيراد من Excel ومصمم التقارير.',
        'Across all active modules' => 'على مستوى جميع الوحدات النشطة',
        'Contact Statement' => 'كشف حساب عميل/مورد',
        'Choose contact for statement' => 'اختر العميل أو المورد لعرض كشف الحساب',
        'Select a contact to display detailed statement.' => 'اختر العميل أو المورد لعرض كشف الحساب التفصيلي.',
        'No transactions for this contact in the selected period.' => 'لا توجد حركات لهذا العميل/المورد خلال الفترة المحددة.',
        'Context' => 'بيئة العمل',
        'Company Context' => 'بيئة عمل الشركة',
        'Report Template' => 'نموذج التقرير',
        'Templates' => 'نماذج التقارير',
        'Saved report designs' => 'تصميمات التقارير المحفوظة',
        'Designed Report Output' => 'مخرجات التقرير المصمم',
        'Chart Type' => 'نوع الرسم البياني',
        'Default Chart Type' => 'نوع الرسم البياني الافتراضي',
        'Default Chart' => 'الرسم البياني الافتراضي',
        'Chart of Accounts' => 'دليل الحسابات',
        'Accounting Reports' => 'التقارير المحاسبية',
        'System Settings' => 'إعدادات النظام',
        'Operations' => 'الحركات',
        'Fixed Assets' => 'الأصول الثابتة',
        'Automatic Entries' => 'القيود التلقائية',

        // Navigation, actions, and generic UI.
        'Home' => 'الرئيسية',
        'Open' => 'فتح',
        'View' => 'عرض',
        'Source' => 'المصدر',
        'Open Source' => 'فتح المستند المصدر',
        'New Entry' => 'قيد جديد',
        'Cancel Edit' => 'إلغاء التعديل',
        'Download Sample XLSX' => 'تحميل نموذج Excel',
        'Export XLSX' => 'تصدير Excel',
        'Import CSV / XLSX' => 'استيراد CSV / Excel',
        'Headers should match field names or labels' => 'يجب أن تطابق العناوين أسماء الحقول أو مسمياتها',
        'Start Import' => 'بدء الاستيراد',
        'Upload Logo' => 'رفع الشعار',
        'If provided, uploaded logo path will replace logo_path automatically.' => 'عند رفع شعار جديد سيتم تحديث مسار الشعار تلقائيا.',
        'Leave blank to generate sequential number automatically.' => 'اتركه فارغا ليقوم النظام بإنشاء الرقم المسلسل تلقائيا.',
        'Auto-generated' => 'يتم إنشاؤه تلقائيا',
        'Choose' => 'اختر',
        'Choose account' => 'اختر الحساب',
        'Select account' => 'اختر الحساب',
        'Optional' => 'اختياري',
        'Show' => 'عرض',
        'Remove' => 'حذف السطر',
        'Add Line' => 'إضافة سطر',
        'Add a line' => 'إضافة سطر',
        'Print Report' => 'طباعة التقرير',
        'Print Entry' => 'طباعة القيد',
        'Register Receipt' => 'تسجيل سند قبض',
        'Register Payment' => 'تسجيل سند دفع',
        'Open the source document to edit this entry.' => 'افتح المستند المصدر لتعديل هذا القيد.',
        'File' => 'الملف',
        'Expected Fields' => 'الحقول المتوقعة',
        'Supported: CSV and XLSX. First non-empty row is used as header.' => 'الصيغ المدعومة: CSV وExcel. يتم استخدام أول صف غير فارغ كعناوين.',
        'From date' => 'من تاريخ',
        'To date' => 'إلى تاريخ',
        'All Vendors' => 'كل الموردين',
        'All Customers' => 'كل العملاء',
        'Line description' => 'بيان السطر',
        'Memo or document reference' => 'مذكرة أو مرجع المستند',
        'You do not have permission to save journal entries.' => 'ليست لديك صلاحية حفظ قيود اليومية.',
        'You do not have permission to save records in this module.' => 'ليست لديك صلاحية حفظ السجلات في هذه الوحدة.',
        'You do not have permission to post or confirm this document.' => 'ليست لديك صلاحية ترحيل أو اعتماد هذا المستند.',
        'You do not have permission to post or change workflow status in this module.' => 'ليست لديك صلاحية الترحيل أو تغيير حالة سير العمل في هذه الوحدة.',
        'You do not have permission to import in this module.' => 'ليست لديك صلاحية الاستيراد في هذه الوحدة.',
        'You do not have permission to delete in this module.' => 'ليست لديك صلاحية الحذف في هذه الوحدة.',
        'Access denied for this module.' => 'تم رفض الوصول إلى هذه الوحدة.',
        'N/A' => 'غير متاح',
        'required' => 'مطلوب',
        'English' => 'الإنجليزية',
        'Arabic' => 'العربية',
        'Commercial Profit Tax Type' => 'نوع ضريبة الأرباح التجارية',
        'Commercial Profit Tax %' => 'نسبة ضريبة الأرباح التجارية',
        'Commercial Profit Tax Amount' => 'قيمة ضريبة الأرباح التجارية',
        'Net Payable' => 'صافي المستحق',
        'Withholding / Commercial Profit Tax' => 'خصم وإضافة / أرباح تجارية',
        'Interest' => 'الفائدة',

        // Roles and permissions.
        'Roles' => 'الأدوار',
        'Role Permissions' => 'صلاحيات الأدوار',
        'Simple Role Permissions' => 'صلاحيات الأدوار المبسطة',
        'Choose the modules this role can open, then enable global edit/import/delete and posting permissions.' => 'اختر الوحدات التي يستطيع هذا الدور فتحها، ثم فعّل صلاحية التعديل/الاستيراد/الحذف وصلاحية الترحيل بشكل عام.',
        'Can Edit All Selected Modules' => 'تعديل كامل على الوحدات المختارة',
        'Can Post All Selected Modules' => 'ترحيل كامل على الوحدات المختارة',
        'Can Open' => 'صلاحية الفتح',
        'Save Permissions' => 'حفظ الصلاحيات',
        'Load' => 'تحميل',
        'Create an active role first.' => 'أنشئ دورًا نشطًا أولًا.',

        // EXcellence medical device rental workspace.
        'EXcellence Accounts' => 'حسابات EXcellence',
        'EXcellence Dashboard' => 'لوحة EXcellence',
        'EXcellence Operations' => 'حركات EXcellence',
        'EXcellence Setup' => 'إعدادات EXcellence',
        'EXcellence Hospitals' => 'مستشفيات EXcellence',
        'Medical Devices' => 'الأجهزة الطبية',
        'Medical Device Operations' => 'عمليات تأجير الأجهزة الطبية',
        'Hospital Expense Payments' => 'مدفوعات مصروفات المستشفيات',
        'EXcellence Administrative Expenses' => 'المصروفات الإدارية - EXcellence',
        'EXcellence Salaries' => 'رواتب EXcellence',
        'Hospitals served by EXcellence medical device rental operations.' => 'المستشفيات التي يتم التعامل معها في عمليات تأجير الأجهزة الطبية.',
        'Medical devices available for hospital procedure rentals with default GL mapping.' => 'الأجهزة الطبية المتاحة للتأجير أثناء العمليات مع ربط الحسابات الافتراضية.',
        'Patient procedure rentals: hospital, device, revenue, hospital share/cost, and automatic GL posting.' => 'تأجير الأجهزة لعمليات المرضى: المستشفى، الجهاز، الإيراد، نصيب/تكلفة المستشفى، والقيود المحاسبية التلقائية.',
        'Payments made to hospitals, either settling accrued operation expenses or direct hospital costs.' => 'مدفوعات المستشفيات سواء لتسوية مصروفات مستحقة على العمليات أو كمصروف مباشر.',
        'Administrative expenses outside patient operations, with direct payment posting.' => 'مصروفات إدارية خارج عمليات المرضى مع قيد سداد مباشر.',
        'Salaries for EXcellence staff outside patient operation costing.' => 'رواتب فريق EXcellence خارج تكلفة عمليات المرضى.',
        'Hospital Code' => 'كود المستشفى',
        'Hospital Name' => 'اسم المستشفى',
        'Device Code' => 'كود الجهاز',
        'Device Name' => 'اسم الجهاز',
        'Medical Device' => 'الجهاز الطبي',
        'Operation Number' => 'رقم العملية',
        'Operation Date' => 'تاريخ العملية',
        'Patient Name' => 'اسم المريض',
        'Patient File No.' => 'رقم ملف المريض',
        'Procedure Name' => 'اسم الإجراء/العملية',
        'Doctor Name' => 'اسم الطبيب',
        'Rental Rate' => 'سعر التأجير',
        'Revenue Amount' => 'مبلغ الإيراد',
        'Hospital Expense Amount' => 'مصروف المستشفى',
        'Net Operation Income' => 'صافي دخل العملية',
        'Rental Revenue Account' => 'حساب إيراد التأجير',
        'Hospital Expense Account' => 'حساب مصروف المستشفى',
        'Related Operation' => 'العملية المرتبطة',
        'Settlement Mode' => 'طريقة التسوية',
        'Settle Accrued Operation Cost' => 'تسوية تكلفة عملية مستحقة',
        'Direct Hospital Expense' => 'مصروف مستشفى مباشر',
        'Hospital Share' => 'نصيب المستشفى',
        'Operation Supplies' => 'مستلزمات العملية',
        'Medical Staff' => 'الطاقم الطبي',
        'Accrued Expense Account' => 'حساب المصروف المستحق',
        'Direct Expense Account' => 'حساب المصروف المباشر',
        'Salary Number' => 'رقم الراتب',
        'Salary Date' => 'تاريخ الراتب',
        'Employee Name' => 'اسم الموظف',
        'Role Title' => 'المسمى الوظيفي',
        'Salary Expense Account' => 'حساب مصروف الرواتب',
        'Create security roles such as accountant, cashier, reviewer, or manager.' => 'إنشاء أدوار صلاحيات مثل المحاسب أو أمين الصندوق أو المراجع أو المدير.',
        'Control module access, editing, deleting, imports, and posting per role.' => 'التحكم في فتح الوحدات والإضافة والتعديل والحذف والاستيراد والترحيل لكل دور.',
        'Role Code' => 'كود الدور',
        'Role Name' => 'اسم الدور',
        'Can Access' => 'صلاحية الفتح',
        'Can Create' => 'صلاحية الإضافة',
        'Can Edit' => 'صلاحية التعديل',
        'Can Delete' => 'صلاحية الحذف',
        'Can Import' => 'صلاحية الاستيراد',
        'Can Post / Confirm' => 'صلاحية الترحيل / الاعتماد',
        'Administrator' => 'مدير النظام',
        'Admin' => 'مدير النظام',
        'Accountant' => 'محاسب',
        'Auditor' => 'مراجع',
        'admin' => 'مدير النظام',
        'accountant' => 'محاسب',
        'auditor' => 'مراجع',

        // Accounting reports and journal entry screens.
        'Accounting Date' => 'تاريخ القيد',
        'Journal Items' => 'بنود القيد',
        'Enter the journal header, then add balanced journal items below.' => 'أدخل بيانات القيد ثم أضف بنودا متوازنة في الأسفل.',
        'Use debit on one side and credit on the other. Revenue and expense lines require a cost center.' => 'استخدم المدين أو الدائن في كل سطر. سطور الإيرادات والمصروفات تتطلب مركز تكلفة.',
        'Debit and credit totals must match for posted entries.' => 'يجب تساوي إجمالي المدين والدائن لترحيل القيود.',
        'No posting accounts are available. Add sub accounts in Chart of Accounts first.' => 'لا توجد حسابات قابلة للترحيل. أضف حسابات فرعية في دليل الحسابات أولا.',
        'This journal entry was generated automatically. Open the source document to change it.' => 'تم إنشاء هذا القيد تلقائيا. افتح المستند المصدر لإجراء التعديل.',
        'Viewing the selected journal entry below. Use Edit from the list to change a draft manual entry.' => 'يتم عرض القيد المحدد أدناه. استخدم تعديل من القائمة لتغيير قيد يدوي في حالة مسودة.',
        'No journal entries found for this company.' => 'لا توجد قيود يومية لهذه الشركة.',
        'No detailed lines available for the selected journal entry.' => 'لا توجد سطور تفصيلية للقيد المحدد.',
        'Entry' => 'القيد',
        'Entries' => 'القيود',
        'Entry No.' => 'رقم القيد',
        'Sales Invoice' => 'فاتورة مبيعات',
        'Student Invoice' => 'فاتورة طالب',
        'Student Receipt' => 'سند قبض طالب',
        'Student Payment Voucher' => 'سند صرف طالب',
        'Purchase Bill' => 'فاتورة مشتريات',
        'Expense' => 'مصروف',
        'Payment' => 'سند دفع/قبض',
        'Prepaid Amortization' => 'إطفاء مصروف مقدم',
        'Opening Entry' => 'قيد افتتاحي',
        'Move ID' => 'معرف الحركة',
        'Line No.' => 'رقم السطر',
        'Lines' => 'السطور',
        'Label (Description)' => 'البيان',
        'Difference' => 'الفرق',
        'Difference 0.00' => 'الفرق 0.00',
        'Balanced' => 'متوازن',
        'Unbalanced:' => 'غير متوازن:',
        'Totals' => 'الإجماليات',
        'Unpost' => 'إلغاء الترحيل',
        'Post' => 'ترحيل',
        'Reset to Draft' => 'إرجاع إلى مسودة',
        'Reverse' => 'عكس القيد',

        // Invoices and document lines.
        'Invoice Lines' => 'بنود الفاتورة',
        'Each line can be taxable or non-taxable, with its own account and cost center.' => 'يمكن أن يكون كل بند خاضعا أو غير خاضع للضريبة مع حساب ومركز تكلفة مستقلين.',
        'Item' => 'البند',
        'Qty' => 'الكمية',
        'Unit Price' => 'سعر الوحدة',
        'Taxable' => 'خاضع للضريبة',
        'Non-taxable' => 'غير خاضع للضريبة',
        'Subtotal after discount' => 'الإجمالي بعد الخصم',
        'VAT:' => 'ضريبة القيمة المضافة:',
        'ZATCA QR (TLV Base64)' => 'رمز الاستجابة السريع لهيئة الزكاة والضريبة والجمارك',

        // Trial balance, statements, and financial reports.
        'As of' => 'كما في',
        'From' => 'من',
        'To' => 'إلى',
        'to' => 'إلى',
        'Opening' => 'الرصيد الافتتاحي',
        'Opening Dr' => 'افتتاحي مدين',
        'Opening Cr' => 'افتتاحي دائن',
        'Period Dr' => 'حركة الفترة مدين',
        'Period Cr' => 'حركة الفترة دائن',
        'Closing Dr' => 'ختامي مدين',
        'Closing Cr' => 'ختامي دائن',
        'Running Balance' => 'الرصيد الجاري',
        'Liabilities + Equity' => 'الالتزامات وحقوق الملكية',
        'Liabilities and Equity' => 'الالتزامات وحقوق الملكية',
        'Total Assets' => 'إجمالي الأصول',
        'Total Liabilities' => 'إجمالي الالتزامات',
        'Total Equity' => 'إجمالي حقوق الملكية',
        'Total Expense' => 'إجمالي المصروفات',
        'Total Inflow' => 'إجمالي التدفقات الداخلة',
        'Total Outflow' => 'إجمالي التدفقات الخارجة',
        'Cash Flow Snapshot (From Payments)' => 'ملخص التدفقات النقدية من المدفوعات',
        'Cash Net Change' => 'صافي التغير النقدي',
        'Net Cash' => 'صافي النقدية',
        'No posted journal lines for the selected period.' => 'لا توجد سطور قيود مرحلة خلال الفترة المحددة.',
        'No trial balance data for the selected period.' => 'لا توجد بيانات لميزان المراجعة خلال الفترة المحددة.',
        'No payment activity for selected period.' => 'لا توجد حركة مدفوعات خلال الفترة المحددة.',
        'No cost center activity for the selected period.' => 'لا توجد حركة على مركز التكلفة خلال الفترة المحددة.',
        'No detailed lines for the selected cost center filter.' => 'لا توجد سطور تفصيلية لمركز التكلفة المحدد.',
        'Revenue and expense journal lines grouped by cost center for posted entries.' => 'سطور قيود الإيرادات والمصروفات مجمعة حسب مركز التكلفة للقيود المرحلة.',

        // Depreciation and prepaid expenses.
        'Run Prepaid Amortization' => 'تشغيل إطفاء المصروفات المقدمة',
        'Create draft monthly entries: debit expense and credit prepaid asset.' => 'إنشاء قيود شهرية مسودة: مدين مصروف ودائن أصل مصروف مقدم.',
        'Cannot run prepaid amortization yet. Please configure prepaid expenses, accounts, and cost centers.' => 'لا يمكن تشغيل إطفاء المصروفات المقدمة الآن. تأكد من إعداد المصروفات المقدمة والحسابات ومراكز التكلفة.',
        'Generate Draft Entries' => 'إنشاء قيود مسودة',
        'Prepaid Expense' => 'مصروف مقدم',
        'Prepaid Account' => 'حساب المصروف المقدم',
        'Start Period' => 'بداية الفترة',
        'Number of Months to Generate' => 'عدد الشهور المطلوب إنشاؤها',
        'No active prepaid expenses' => 'لا توجد مصروفات مقدمة نشطة',
        'Create monthly depreciation entries as draft journal lines for review.' => 'إنشاء قيود إهلاك شهرية كقيود مسودة للمراجعة.',
        'Depreciation Run Details' => 'تفاصيل تشغيل الإهلاك',
        'No run selected.' => 'لم يتم اختيار تشغيل.',
        'No depreciation lines available for this run.' => 'لا توجد سطور إهلاك لهذا التشغيل.',
        'Previous Accumulated' => 'مجمع الإهلاك السابق',
        'Depreciation Amount' => 'قيمة الإهلاك',
        'New Accumulated' => 'مجمع الإهلاك الجديد',

        // Portal and workspaces.
        'ERP Portal' => 'بوابة النظام',
        'General Accounting' => 'الحسابات العامة',
        'Human Resources' => 'الموارد البشرية',
        'Student Accounts' => 'حسابات الطلاب',
        'Student' => 'الطالب',
        'Stage / Grade' => 'المرحلة / الصف',
        'Build sections and multi-column comparisons per report template.' => 'أنشئ بنودا وأعمدة مقارنة متعددة لكل نموذج تقرير.',
        'No data available for this section.' => 'لا توجد بيانات متاحة لهذا البند.',

        // Common stored values.
        'draft' => 'مسودة',
        'posted' => 'مرحل',
        'issued' => 'مصدر',
        'approved' => 'معتمد',
        'cancelled' => 'ملغي',
        'partially_paid' => 'مدفوع جزئيا',
        'paid' => 'مدفوع',
        'active' => 'نشط',
        'inactive' => 'غير نشط',
        'cash' => 'نقدي',
        'bank' => 'بنك',
        'card' => 'بطاقة',
        'transfer' => 'تحويل',
        'in' => 'قبض',
        'out' => 'صرف',
        'asset' => 'أصل',
        'liability' => 'التزام',
        'equity' => 'حقوق ملكية',
        'revenue' => 'إيراد',
        'expense' => 'مصروف',
        'customer' => 'عميل',
        'vendor' => 'مورد',
        'both' => 'عميل ومورد',
        'OUT' => 'صرف',
    ];
}

function app_i18n_accounting_coverage_map(): array
{
    return [
        // Core UI terminology.
        'Dashboard' => 'لوحة المؤشرات',
        'Dashboards' => 'لوحات المؤشرات',
        'Dashboard Report' => 'تقرير لوحة المؤشرات',
        'Period:' => 'الفترة:',
        'to' => 'إلى',
        'Comparison:' => 'فترة المقارنة:',
        'Comparison as of' => 'المقارنة كما في',
        'row(s)' => 'صف',
        'template(s)' => 'قالب',
        'entry(s), newest accounting dates first' => 'قيد، الأحدث حسب تاريخ القيد أولا',
        'Modules' => 'الوحدات',
        'modules' => 'وحدات',
        'Module' => 'الوحدة',
        'Module Portal' => 'بوابة الوحدات',
        'Module :' => 'الوحدة :',
        'Module Scope' => 'نطاق الوحدة',
        'Module Activity' => 'نشاط الوحدات',
        'Quick totals per module' => 'إجماليات سريعة لكل وحدة',
        'Across all active modules' => 'على مستوى كل الوحدات النشطة',
        'Use your account to access modules, imports, and report designer.' => 'استخدم حسابك للوصول إلى الوحدات والاستيراد ومصمم التقارير.',
        'Full accounting suite with CRUD modules, Excel import, and report designer.' => 'منظومة محاسبية متكاملة للإدخال والمراجعة والاستيراد من Excel وتصميم التقارير.',
        'Choose a module to continue' => 'اختر وحدة للمتابعة',
        'General Accounts' => 'الحسابات العامة',
        'General Accounting' => 'الحسابات العامة',
        'Journals, invoices, reports' => 'قيود وفواتير وتقارير',
        'Companies, users & config' => 'الشركات والمستخدمون والإعدادات',
        'Employees & payroll' => 'الموظفون والرواتب',
        'Change password & preferences' => 'تغيير كلمة المرور والتفضيلات',
        'My Profile' => 'ملفي الشخصي',
        'Cashier sessions and receipts' => 'جلسات أمين الصندوق والإيصالات',
        'Tuition invoices & payments' => 'فواتير ومتحصلات المصروفات الدراسية',
        'HR Reports' => 'تقارير الموارد البشرية',
        'Student Reports' => 'تقارير حسابات الطلاب',
        'Back to Home' => 'العودة للرئيسية',
        'Dashboard' => 'لوحة المؤشرات',
        'Report Designer' => 'مصمم التقارير',
        'Point of Sale' => 'نقاط البيع',
        'Ledger, Journal Book, Cost Center, Trial Balance, Income Statement, Financial Position, ECL, and Cash Flow' => 'الأستاذ العام ودفتر اليومية ومراكز التكلفة وميزان المراجعة وقائمة الدخل وقائمة المركز المالي والخسائر الائتمانية المتوقعة والتدفقات النقدية',
        'records' => 'سجلات',
        'Record' => 'سجل',
        'Manual entry for this module.' => 'إدخال يدوي لهذه الوحدة.',
        'No records found for this module and company context.' => 'لا توجد سجلات لهذه الوحدة ضمن سياق الشركة الحالي.',
        'Close' => 'إغلاق',
        'x' => 'إغلاق',
        'Open / close' => 'فتح / إغلاق',
        'Expand All' => 'فتح الكل',
        'Collapse All' => 'طي الكل',
        'Search products, SKU, barcode' => 'بحث بالمنتج أو كود الصنف أو الباركود',
        'Stock' => 'المخزون',
        'Internal note for this entry' => 'ملاحظة داخلية لهذا القيد',
        'Customer note' => 'ملاحظة العميل',
        'Receipt / customer note' => 'ملاحظة الإيصال / العميل',
        'Company Logo' => 'شعار الشركة',
        'Logo' => 'الشعار',
        'Orion Accounts ERP' => 'أوريون للحسابات ERP',
        'Original Orion UI' => 'واجهة أوريون الأصلية',
        'Choose the active system interface theme while keeping the original Orion UI available.' => 'اختيار واجهة النظام النشطة مع إبقاء واجهة أوريون الأصلية متاحة.',
        'Develop Finance UI' => 'واجهة Develop المالية',
        'Themes' => 'الثيمات',
        'Theme' => 'المظهر',
        'Theme Code' => 'كود الثيم',
        'Theme Name' => 'اسم الثيم',
        'UI Style' => 'نمط الواجهة',
        'Color Mode' => 'وضع الألوان',
        'Use As Current Theme' => 'استخدامه كثيم حالي',
        'Light' => 'فاتح',
        'Dark' => 'داكن',

        // Chart of accounts and financial statements.
        'Chart of Accounts' => 'دليل الحسابات',
        'Chart of Accounts Tree' => 'شجرة دليل الحسابات',
        'Create the accounting tree and opening balances.' => 'إنشاء دليل الحسابات والأرصدة الافتتاحية.',
        'Parent Account' => 'الحساب الرئيسي',
        'Parent Code' => 'كود الحساب الرئيسي',
        'Add sub account' => 'إضافة حساب فرعي',
        'Add Sub Account' => 'إضافة حساب فرعي',
        'The child account code is generated from the selected parent account.' => 'يتم اقتراح كود الحساب الفرعي بناء على الحساب الرئيسي المحدد.',
        'Auto-generated; you can adjust it before saving if needed.' => 'كود مقترح تلقائيا، ويمكن تعديله قبل الحفظ عند الحاجة.',
        'Type / Group / Balance / Active' => 'النوع / بند القوائم / الرصيد / الحالة',
        'No accounts found for this company context.' => 'لا توجد حسابات في سياق الشركة الحالي.',
        'Account Balances' => 'أرصدة الحسابات',
        'Account Codes (comma separated)' => 'أكواد الحسابات مفصولة بفواصل',
        'Offset Account' => 'حساب التسوية',
        'Offset Account (For unpaid / partial only)' => 'حساب التسوية للحالات غير المسددة أو الجزئية فقط',
        'Posting Accounts' => 'حسابات التوجيه المحاسبي',
        'Posting Account' => 'حساب التوجيه',
        'Map default AR, AP, accrued, prepaid, depreciation, and IFRS 16 lease accounts per company.' => 'ربط الحسابات الافتراضية للمدينين والدائنين والمستحقات والمقدمات والإهلاك وعقود IFRS 16 لكل شركة.',
        'Sales Receivable Account' => 'حساب العملاء',
        'Purchase Payable Account' => 'حساب الموردين',
        'Accrued Expense Account' => 'حساب المصروفات المستحقة',
        'Prepaid Expense Account' => 'حساب المصروفات المقدمة',
        'Financial Statements' => 'القوائم المالية',
        'Financial Statement Groups' => 'بنود القوائم المالية',
        'Code statement line groups used by the Income Statement and Statement of Financial Position.' => 'تكويد بنود التقارير المستخدمة في قائمة الدخل وقائمة المركز المالي.',
        'Some accounts are not mapped to a financial statement group yet and are shown under Unmapped Accounts.' => 'توجد حسابات لم يتم ربطها بعد ببند من بنود القوائم المالية، لذلك تظهر ضمن الحسابات غير المربوطة.',
        'No comparison' => 'بدون مقارنة',
        'Show comparison' => 'إظهار المقارنة',
        'Comparison Period' => 'فترة المقارنة',
        'Comparison From Date' => 'بداية فترة المقارنة',
        'Comparison To Date' => 'نهاية فترة المقارنة',
        'Total Equity and Liabilities' => 'إجمالي حقوق الملكية والالتزامات',
        'Balance Sheet' => 'قائمة المركز المالي',
        'Income Statement' => 'قائمة الدخل',
        'Expected Credit Loss' => 'الخسائر الائتمانية المتوقعة',

        // IFRS 16 leases.
        'IFRS16 Leases' => 'عقود الإيجار IFRS 16',
        'IFRS16 Lease' => 'عقد إيجار IFRS 16',
        'IFRS16 Lease Schedule' => 'جدول عقد الإيجار IFRS 16',
        'IFRS16 Payment Line' => 'دفعة إيجار IFRS 16',
        'Calculate IFRS 16 right-of-use assets, lease liabilities, interest, depreciation, and draft journal entries.' => 'احتساب أصل حق الاستخدام والتزام الإيجار والفوائد والإهلاك وقيود اليومية المسودة طبقا لـ IFRS 16.',
        'Generated IFRS 16 payment-date schedule and recurring draft journals.' => 'جدول دفعات IFRS 16 والقيود الدورية المسودة.',
        'Initial recognition plus recurring interest and ROU depreciation journals. Bank payment is posted manually from Payments.' => 'إثبات أولي مع قيود الفائدة الدورية وإهلاك أصل حق الاستخدام. يتم إثبات السداد البنكي يدويا من المدفوعات.',
        'Generate IFRS16 Draft Entries' => 'إنشاء قيود IFRS 16 مسودة',
        'Lease' => 'عقد الإيجار',
        'Lease Number' => 'رقم عقد الإيجار',
        'Lease Name' => 'اسم عقد الإيجار',
        'Contract Number' => 'رقم العقد',
        'Contract Type' => 'نوع العقد',
        'Lessor / Vendor' => 'المؤجر / المورد',
        'Commencement Date' => 'تاريخ بداية العقد',
        'First Payment Date' => 'تاريخ أول دفعة',
        'Payment Frequency' => 'دورية السداد',
        'Monthly' => 'شهري',
        'Quarterly' => 'ربع سنوي',
        'Semiannual' => 'نصف سنوي',
        'Annual' => 'سنوي',
        'Payment Account' => 'حساب السداد',
        'Default Payment Amount' => 'قيمة الدفعة الافتراضية',
        'Number of Payments (0 = until end date)' => 'عدد الدفعات (0 حتى تاريخ نهاية العقد)',
        'Payments At/Before Commencement' => 'دفعات في أو قبل تاريخ بداية العقد',
        'Initial Direct Costs' => 'تكاليف مباشرة أولية',
        'Restoration / Dismantling Cost' => 'تكلفة الإزالة أو إعادة الموقع',
        'Lease Incentives Received' => 'حوافز إيجار مستلمة',
        'Lease Incentives Received' => 'حوافز الإيجار المستلمة',
        'Lease Incentives' => 'حوافز الإيجار',
        'Benchmark Rate %' => 'معدل الأساس %',
        'Credit Spread %' => 'هامش المخاطر الائتمانية %',
        'Manual Discount Rate Override %' => 'معدل الخصم اليدوي %',
        'Manual Rate Override' => 'معدل يدوي بديل',
        'Manual Discount Rate Override %' => 'معدل خصم يدوي بديل %',
        'IBR Used %' => 'معدل الاقتراض الإضافي المستخدم %',
        'PV Lease Liability' => 'القيمة الحالية لالتزام الإيجار',
        'PV at Commencement' => 'القيمة الحالية في تاريخ بداية العقد',
        'Initial ROU Asset' => 'أصل حق الاستخدام الأولي',
        'ROU Asset Account' => 'حساب أصل حق الاستخدام',
        'ROU Asset NBV' => 'صافي القيمة الدفترية لأصل حق الاستخدام',
        'ROU NBV' => 'صافي أصل حق الاستخدام',
        'Lease Liability Account' => 'حساب التزام الإيجار',
        'Lease Incentive Account' => 'حساب حوافز الإيجار',
        'Restoration Provision Account' => 'حساب مخصص الإزالة وإعادة الموقع',
        'IFRS16 ROU Asset Account' => 'حساب أصل حق الاستخدام IFRS 16',
        'IFRS16 Lease Liability Account' => 'حساب التزام الإيجار IFRS 16',
        'IFRS16 Interest Expense Account' => 'حساب مصروف الفوائد IFRS 16',
        'IFRS16 Depreciation Expense Account' => 'حساب مصروف إهلاك IFRS 16',
        'IFRS16 Accumulated Depreciation Account' => 'حساب مجمع إهلاك IFRS 16',
        'IFRS16 Restoration Provision Account' => 'حساب مخصص الإزالة IFRS 16',
        'IFRS16 Lease Incentive Account' => 'حساب حوافز الإيجار IFRS 16',
        'Interest Expense' => 'مصروف الفوائد',
        'Interest Expense Account' => 'حساب مصروف الفوائد',
        'Depreciation Expense' => 'مصروف الإهلاك',
        'Depreciation Expense Account' => 'حساب مصروف الإهلاك',
        'Daily Depreciation' => 'الإهلاك اليومي',
        'Monthly Depreciation Approx.' => 'الإهلاك الشهري التقريبي',
        'Liability Reduction' => 'تخفيض الالتزام',
        'Closing Liability' => 'رصيد الالتزام الختامي',
        'Opening Liability' => 'رصيد الالتزام الافتتاحي',
        'Closing Liability Check' => 'فحص رصيد الالتزام الختامي',
        'Lease Term Days' => 'مدة الإيجار بالأيام',
        'Lease Term Months' => 'مدة الإيجار بالشهور',
        'Discount Factor' => 'معامل الخصم',
        'Payment Input PV' => 'القيمة الحالية للدفعة المدخلة',
        'Schedule Lines' => 'سطور الجدول',
        'Upload Payments' => 'رفع جدول الدفعات',
        'No schedule lines generated yet.' => 'لم يتم إنشاء سطور الجدول بعد.',

        // Expected credit loss.
        'ECL Countries' => 'دول الخسائر الائتمانية المتوقعة',
        'ECL Country' => 'دولة الخسائر الائتمانية المتوقعة',
        'ECL Default Rates' => 'معدلات التعثر الافتراضية',
        'ECL Receivable Buckets' => 'شرائح أعمار المدينين للخسائر الائتمانية',
        'ECL Report Categories' => 'تصنيفات تقرير الخسائر الائتمانية',
        'No ECL report categories are configured yet.' => 'لم يتم إعداد تصنيفات تقرير الخسائر الائتمانية بعد.',
        'Country ratings, macro scenario factors, and scenario weights for expected credit loss reports.' => 'تصنيفات الدول وعوامل السيناريوهات الاقتصادية وأوزانها لتقارير الخسائر الائتمانية المتوقعة.',
        'Used for country ratings, scenario weights, and forward-looking PD.' => 'يستخدم لتصنيفات الدول وأوزان السيناريوهات واحتمالات التعثر المستقبلية.',
        'Aging buckets and forward-looking loss rates used for sales receivables.' => 'شرائح أعمار الديون ومعدلات الخسارة المستقبلية المستخدمة في أرصدة العملاء.',
        'Map report lines to ERP sources, account balances, and ECL loss models.' => 'ربط بنود التقرير بمصادر النظام وأرصدة الحسابات ونماذج الخسائر الائتمانية.',
        '12-Month Country Forward-Looking' => 'احتمال تعثر مستقبلي 12 شهرا حسب الدولة',
        'Country' => 'الدولة',
        'Country Code' => 'كود الدولة',
        'Country Name' => 'اسم الدولة',
        'Country Override' => 'دولة بديلة',
        'Fitch' => 'فيتش',
        'Fitch Rating' => 'تصنيف فيتش',
        'Moody\'s' => 'موديز',
        'Moody\'s Rating' => 'تصنيف موديز',
        'S&P' => 'ستاندرد آند بورز',
        'S&P Rating' => 'تصنيف ستاندرد آند بورز',
        'Rating Code' => 'كود التصنيف',
        'One-Year PD %' => 'احتمال التعثر لسنة واحدة %',
        'Observed one-year probability of default by rating agency and rating code.' => 'احتمال التعثر الملحوظ لمدة سنة حسب وكالة التصنيف وكود التصنيف.',
        'Base Weight %' => 'وزن السيناريو الأساسي %',
        'Base Scenario Factor' => 'معامل السيناريو الأساسي',
        'Best Weight %' => 'وزن السيناريو المتفائل %',
        'Best Scenario Factor' => 'معامل السيناريو المتفائل',
        'Worst Weight %' => 'وزن السيناريو المتشائم %',
        'Worst Scenario Factor' => 'معامل السيناريو المتشائم',
        'Receivable Aging Buckets' => 'شرائح أعمار أرصدة العملاء',
        'Receivable Aging Bucket' => 'شريحة عمر الدين',
        'Bucket Code' => 'كود الشريحة',
        'Bucket Name' => 'اسم الشريحة',
        'Min Days' => 'أدنى عدد أيام',
        'Max Days (0 = open)' => 'أقصى عدد أيام (0 مفتوح)',
        'Days' => 'الأيام',
        'Days Count' => 'عدد الأيام',
        'Annual Balance Days' => 'أيام الرصيد السنوي',
        'Expected loss rate' => 'معدل الخسارة المتوقع',
        'Expected Loss Rate' => 'معدل الخسارة المتوقع',
        'Loss Rate' => 'معدل الخسارة',
        'Loss Rate %' => 'معدل الخسارة %',
        'LGD %' => 'نسبة الخسارة عند التعثر %',
        'Loss Model' => 'نموذج احتساب الخسارة',
        'Manual Amount' => 'مبلغ يدوي',
        'Rate Override %' => 'معدل بديل %',
        'Manual Rate Override' => 'معدل بديل يدوي',
        'Sales Receivables' => 'أرصدة العملاء',
        'Gross' => 'إجمالي الرصيد',
        'Gross carrying amount' => 'إجمالي القيمة الدفترية',
        'Forward Looking ECL' => 'مخصص الخسائر الائتمانية المستقبلية',
        'Forward Looking ECL Allowance' => 'مخصص الخسائر الائتمانية المستقبلية',

        // POS.
        'Point of Sale' => 'نقاط البيع',
        'POS Workspace' => 'مساحة عمل نقاط البيع',
        'POS Terminal' => 'شاشة نقاط البيع',
        'POS Registers' => 'نقاط البيع',
        'POS Sessions' => 'جلسات نقاط البيع',
        'POS Orders' => 'أوامر نقاط البيع',
        'POS Cash In/Out' => 'حركات نقدية لنقاط البيع',
        'POS Sales Revenue Account' => 'حساب إيرادات نقاط البيع',
        'POS Cash Overage Gain Account' => 'حساب أرباح زيادة النقدية بنقاط البيع',
        'POS Cash Shortage Loss Account' => 'حساب خسائر عجز النقدية بنقاط البيع',
        'POS session opened successfully.' => 'تم فتح جلسة نقاط البيع بنجاح.',
        'POS order validated and posted successfully.' => 'تم اعتماد أمر نقاط البيع وترحيله بنجاح.',
        'POS cash movement posted successfully.' => 'تم ترحيل الحركة النقدية بنجاح.',
        'POS session closed successfully.' => 'تم إغلاق جلسة نقاط البيع بنجاح.',
        'Company context is required for POS.' => 'يجب تحديد الشركة لاستخدام نقاط البيع.',
        'You do not have permission to validate POS orders.' => 'ليست لديك صلاحية اعتماد أوامر نقاط البيع.',
        'You do not have permission to post POS cash movements.' => 'ليست لديك صلاحية ترحيل الحركات النقدية لنقاط البيع.',
        'You do not have permission to close POS sessions.' => 'ليست لديك صلاحية إغلاق جلسات نقاط البيع.',
        'Configure point-of-sale registers, cash boxes, receipt behavior, and posting defaults.' => 'إعداد نقاط البيع والخزائن وسلوك الإيصالات وحسابات التوجيه الافتراضية.',
        'Cashier opening and closing sessions with expected cash and cash difference tracking.' => 'جلسات فتح وإغلاق أمين الصندوق مع تتبع النقدية المتوقعة وفروق النقدية.',
        'Posted point-of-sale orders, receipts, refunds, split payments, and journal links.' => 'أوامر نقاط البيع المرحلة والإيصالات والمرتجعات والدفعات المقسمة وروابط القيود.',
        'Register cash adjustments posted directly to the general ledger.' => 'تسجيل تسويات النقدية وترحيلها مباشرة إلى الأستاذ العام.',
        'Sessions, orders, refunds, split payments, and GL posting' => 'جلسات وأوامر ومرتجعات ودفعات مقسمة وترحيل للأستاذ العام',
        'Create at least one active POS register linked to a cash drawer and cost center.' => 'أنشئ نقطة بيع نشطة واحدة على الأقل مرتبطة بخزينة ومركز تكلفة.',
        'Create POS Register' => 'إنشاء نقطة بيع',
        'Open POS Session' => 'فتح جلسة نقاط بيع',
        'Open Session' => 'فتح الجلسة',
        'Close Session' => 'إغلاق الجلسة',
        'Post Cash Movement' => 'ترحيل حركة نقدية',
        'Current Order' => 'الأمر الحالي',
        'Recent POS Orders' => 'أحدث أوامر نقاط البيع',
        'No POS orders yet.' => 'لا توجد أوامر نقاط بيع بعد.',
        'No products found.' => 'لا توجد منتجات مطابقة.',
        'Cart is empty.' => 'سلة البيع فارغة.',
        'Walk-in Customer' => 'عميل نقدي',
        'Order' => 'أمر بيع',
        'Orders' => 'الأوامر',
        'Order Number' => 'رقم الأمر',
        'Order Date' => 'تاريخ الأمر',
        'Order Type' => 'نوع الأمر',
        'Sale' => 'بيع',
        'Refund' => 'مرتجع',
        'Refunded' => 'مردود',
        'Original Order' => 'الأمر الأصلي',
        'Register' => 'نقطة البيع',
        'Registers' => 'نقاط البيع',
        'Register Code' => 'كود نقطة البيع',
        'Register Name' => 'اسم نقطة البيع',
        'Cash Drawer' => 'الخزينة',
        'Default Bank/Card Account' => 'حساب البنك/البطاقات الافتراضي',
        'Default Customer' => 'العميل الافتراضي',
        'Receipt Footer' => 'تذييل الإيصال',
        'Prices Include Tax' => 'الأسعار شاملة الضريبة',
        'Allow Negative Stock' => 'السماح بالمخزون السالب',
        'Session ID' => 'معرف الجلسة',
        'Session Number' => 'رقم الجلسة',
        'Opened At' => 'وقت الفتح',
        'Closed At' => 'وقت الإغلاق',
        'Opening Cash' => 'رصيد النقدية الافتتاحي',
        'Expected Cash' => 'النقدية المتوقعة',
        'Opening Cash Counted' => 'النقدية المعدودة عند الفتح',
        'Closing Cash Counted' => 'النقدية المعدودة عند الإغلاق',
        'Cash Difference' => 'فرق النقدية',
        'Cash In' => 'إيداع نقدي',
        'Cash Out' => 'سحب نقدي',
        'Cash In / Out' => 'إيداع / سحب نقدي',
        'Closing Notes' => 'ملاحظات الإغلاق',
        'Add Split' => 'إضافة دفعة مقسمة',
        'Invoice requested' => 'مطلوب فاتورة',
        'Invoice Requested' => 'مطلوب فاتورة',
        'Receipt Number' => 'رقم الإيصال',
        'Reason' => 'السبب',
        'Price' => 'السعر',
        'Disc %' => 'نسبة الخصم %',
        'Taxable Amount' => 'المبلغ الخاضع للضريبة',
        'Included' => 'شامل',
        'For refunds' => 'للمرتجعات',
        'Validate Payment' => 'اعتماد السداد',
        'Commercial Profit Tax' => 'ضريبة الخصم والإضافة',
        'VAT' => 'ضريبة القيمة المضافة',
        'ZATCA QR / Barcode' => 'رمز زاتكا / الباركود',
        'Document barcode' => 'باركود المستند',
        'SAR' => 'ريال سعودي',
        'SKU' => 'كود الصنف',
        'All Categories' => 'كل التصنيفات',

        // Student accounts and academic setup.
        'Students' => 'الطلاب',
        'Student Invoices' => 'فواتير الطلاب',
        'Student Receipts' => 'سندات قبض الطلاب',
        'Student Payment Vouchers' => 'سندات صرف الطلاب',
        'Student Revenue Types' => 'بنود إيرادات الطلاب',
        'Student GL Settings' => 'إعدادات ربط حسابات الطلاب',
        'Student Stages' => 'المراحل الدراسية',
        'Student Grades' => 'الصفوف الدراسية',
        'Student Classes' => 'الفصول الدراسية',
        'Student Fee Plans' => 'خطط مصروفات الطلاب',
        'Student master data, guardian contact, stage, grade, and class.' => 'بيانات الطلاب الأساسية وولي الأمر والمرحلة والصف والفصل.',
        'Student billing with GL posting, collection tracking, tax, and ZATCA QR print.' => 'فوترة الطلاب مع الترحيل للأستاذ العام وتتبع التحصيل والضريبة وطباعة رمز زاتكا.',
        'Receipt vouchers for student invoices or advance collections.' => 'سندات قبض لفواتير الطلاب أو التحصيلات المقدمة.',
        'Payment vouchers/refunds to students or guardians with GL offset account.' => 'سندات صرف أو رد مبالغ للطلاب أو أولياء الأمور مع حساب تسوية.',
        'Map student receivables, advances, refunds, default revenue, and cost center to General Ledger.' => 'ربط حسابات الطلاب المدينة والمقدمات والمرتجعات والإيرادات الافتراضية ومركز التكلفة بالأستاذ العام.',
        'Tuition, registration, transport, books, uniforms, activities, exams, and other student revenues.' => 'مصروفات الدراسة والتسجيل والنقل والكتب والزي والأنشطة والاختبارات وباقي إيرادات الطلاب.',
        'Saudi school stages such as KG, Primary, Intermediate, and Secondary.' => 'المراحل الدراسية مثل الروضة والابتدائي والمتوسط والثانوي.',
        'Grades are linked to one stage, preventing grade 12 from appearing under KG.' => 'يرتبط كل صف بمرحلة واحدة لمنع ظهور الصفوف تحت مرحلة غير صحيحة.',
        'Class/section definitions linked to academic year, stage, and grade.' => 'تعريفات الفصول والشعب المرتبطة بالعام الدراسي والمرحلة والصف.',
        'Stage/grade fee plans with installment settings and revenue type mapping.' => 'خطط مصروفات حسب المرحلة والصف مع إعدادات الأقساط وربط بنود الإيراد.',
        'Academic Year' => 'العام الدراسي',
        'Stage' => 'المرحلة',
        'Stage Code' => 'كود المرحلة',
        'Stage Name' => 'اسم المرحلة',
        'Grade' => 'الصف',
        'Grade Code' => 'كود الصف',
        'Grade Name' => 'اسم الصف',
        'Class' => 'الفصل',
        'Class Code' => 'كود الفصل',
        'Class Name' => 'اسم الفصل',
        'Student Number' => 'رقم الطالب',
        'Student Name' => 'اسم الطالب',
        'Guardian / Customer' => 'ولي الأمر / العميل',
        'Enrollment Date' => 'تاريخ الالتحاق',
        'Fee Plan Code' => 'كود خطة المصروفات',
        'Fee Plan Name' => 'اسم خطة المصروفات',
        'Revenue Type' => 'نوع الإيراد',
        'Revenue Type Code' => 'كود نوع الإيراد',
        'Revenue Type Name' => 'اسم نوع الإيراد',
        'Installments Count' => 'عدد الأقساط',
        'Due Day' => 'يوم الاستحقاق',
        'Student Receivable Account' => 'حساب ذمم الطلاب',
        'Student Advance / Unearned Account' => 'حساب مقدمات الطلاب / إيرادات غير مكتسبة',
        'Refund / Discount Offset Account' => 'حساب تسوية المرتجعات / الخصومات',
        'Default Revenue Account' => 'حساب الإيراد الافتراضي',
        'Voucher Number' => 'رقم السند',
        'Voucher Date' => 'تاريخ السند',
        'Receipt Date' => 'تاريخ القبض',
        'Activities' => 'الأنشطة',
        'Registration' => 'التسجيل',
        'Transport' => 'النقل',
        'Books' => 'الكتب',
        'Uniform' => 'الزي المدرسي',
        'Exam' => 'الاختبارات',
        'Tuition' => 'المصروفات الدراسية',
        'Cafeteria' => 'المقصف',
        'Withdrawn' => 'منسحب',
        'Graduated' => 'متخرج',
        'Suspended' => 'موقوف',

        // HR and payroll.
        'HR Departments' => 'إدارات الموارد البشرية',
        'Human resources department structure and cost center mapping.' => 'هيكل إدارات الموارد البشرية وربط مراكز التكلفة.',
        'Job Titles' => 'المسميات الوظيفية',
        'Job titles, grades, and department assignment.' => 'المسميات والدرجات الوظيفية وربط الإدارات.',
        'Employee Contracts' => 'عقود الموظفين',
        'Track employee contracts, salaries, departments, and job assignments.' => 'متابعة عقود الموظفين والرواتب والإدارات والتكليفات الوظيفية.',
        'Work Shifts' => 'ورديات العمل',
        'Define working schedules used by attendance records.' => 'تعريف جداول العمل المستخدمة في سجلات الحضور.',
        'Leave Types' => 'أنواع الإجازات',
        'Define paid and unpaid leave policies and balances.' => 'تعريف سياسات وأرصدة الإجازات المدفوعة وغير المدفوعة.',
        'Leave Requests' => 'طلبات الإجازة',
        'Employee leave requests and approval status.' => 'طلبات إجازات الموظفين وحالات الاعتماد.',
        'Daily attendance, lateness, absence, and hours worked.' => 'الحضور اليومي والتأخير والغياب وساعات العمل.',
        'Salary Components' => 'مكونات الراتب',
        'Reusable earnings, allowances, deductions, and payroll posting accounts.' => 'بنود مستحقات وبدلات واستقطاعات قابلة لإعادة الاستخدام مع حسابات التوجيه.',
        'Payroll Items' => 'بنود مسير الرواتب',
        'Detailed payroll lines by employee and salary component.' => 'سطور تفصيلية لمسير الرواتب حسب الموظف ومكون الراتب.',
        'Employee' => 'الموظف',
        'Department Code' => 'كود الإدارة',
        'Department Name' => 'اسم الإدارة',
        'Job Code' => 'كود الوظيفة',
        'Job Title' => 'المسمى الوظيفي',
        'Contract Number' => 'رقم العقد',
        'National ID / Iqama' => 'رقم الهوية / الإقامة',
        'Insurance Number' => 'رقم التأمينات',
        'Contract Type' => 'نوع العقد',
        'Full Time' => 'دوام كامل',
        'Part Time' => 'دوام جزئي',
        'Temporary' => 'مؤقت',
        'Consultant' => 'استشاري',
        'Basic Salary' => 'الراتب الأساسي',
        'Employer Cost' => 'تكلفة صاحب العمل',
        'Shift' => 'الوردية',
        'Shift Code' => 'كود الوردية',
        'Shift Name' => 'اسم الوردية',
        'Start Time' => 'وقت البداية',
        'End Time' => 'وقت النهاية',
        'Working Hours' => 'ساعات العمل',
        'Attendance' => 'الحضور',
        'Attendance Number' => 'رقم سجل الحضور',
        'Attendance Date' => 'تاريخ الحضور',
        'Check In' => 'وقت الحضور',
        'Check Out' => 'وقت الانصراف',
        'Hours Worked' => 'ساعات العمل الفعلية',
        'Present' => 'حاضر',
        'Absent' => 'غائب',
        'Late' => 'متأخر',
        'Leave' => 'إجازة',
        'Holiday' => 'عطلة',
        'Leave Type' => 'نوع الإجازة',
        'Leave Type Code' => 'كود نوع الإجازة',
        'Leave Type Name' => 'اسم نوع الإجازة',
        'Paid Leave' => 'إجازة مدفوعة',
        'Unpaid' => 'غير مدفوعة',
        'Request Number' => 'رقم الطلب',
        'Pending' => 'قيد المراجعة',
        'Rejected' => 'مرفوض',
        'Salary Component' => 'مكون الراتب',
        'Component Code' => 'كود المكون',
        'Component Name' => 'اسم المكون',
        'Earning' => 'استحقاق',
        'Deduction' => 'استقطاع',
        'Allowance' => 'بدل',
        'Calculation' => 'طريقة الاحتساب',
        'Fixed' => 'ثابت',
        'Percent' => 'نسبة',
        'Percent %' => 'النسبة %',
        'Payroll Run' => 'مسير الرواتب',

        // Prepaids, fixed assets, inventory, and common document terms.
        'Prepaid Expenses' => 'المصروفات المقدمة',
        'Capitalize prepaid costs and generate monthly amortization as draft journal entries.' => 'رسملة المصروفات المقدمة وإنشاء قيود إطفاء شهرية مسودة.',
        'Generate draft amortization entries: Dr expense / Cr prepaid asset.' => 'إنشاء قيود إطفاء مسودة: مدين مصروف / دائن مصروف مقدم.',
        'Prepaid Number' => 'رقم المصروف المقدم',
        'Prepaid Date' => 'تاريخ المصروف المقدم',
        'Prepaid Base Amount' => 'أساس المصروف المقدم',
        'Amortization Start Date' => 'تاريخ بداية الإطفاء',
        'Amortization Expense Account' => 'حساب مصروف الإطفاء',
        'Generated Amortization' => 'الإطفاء المنشأ',
        'Month No' => 'رقم الشهر',
        'Assets' => 'الأصول',
        'Asset Code' => 'كود الأصل',
        'Asset Name' => 'اسم الأصل',
        'Custodian' => 'العهدة',
        'Location' => 'الموقع',
        'Serial Number' => 'الرقم المسلسل',
        'Depreciation' => 'الإهلاك',
        'Accumulated Depreciation' => 'مجمع الإهلاك',
        'Taxable Expense' => 'مصروف خاضع للضريبة',
        'Taxable Prepaid Expense' => 'مصروف مقدم خاضع للضريبة',
        'Expense Date' => 'تاريخ المصروف',
        'Expense Number' => 'رقم المصروف',
        'Expense Type' => 'نوع المصروف',
        'Direct expenses, petty cash payments, and reimbursable costs.' => 'مصروفات مباشرة وعهد نقدية ومصروفات قابلة للاسترداد.',
        'Tax Posting Account' => 'حساب توجيه الضريبة',
        'VAT Number (15 digits)' => 'الرقم الضريبي (15 رقما)',
        'IBAN' => 'رقم الآيبان',
        'Cash Accounts' => 'الخزائن',
        'Manage cash boxes, petty cash custodians, and cash balances.' => 'إدارة الخزائن وعهد النقدية وأرصدة الصندوق.',
        'Cash Name' => 'اسم الخزينة',
        'Default Amount' => 'المبلغ الافتراضي',
        'Default Rental Rate' => 'سعر التأجير الافتراضي',
        'Default Cost Center' => 'مركز التكلفة الافتراضي',
        'Duration (Months)' => 'المدة بالشهور',
        'Issued' => 'مصدر',
        'Item Number' => 'رقم البند',
        'Monthly Amount' => 'المبلغ الشهري',
        'Sales Revenue Account' => 'حساب إيرادات المبيعات',
        'Sequence No' => 'رقم التسلسل',
        'Source Type' => 'نوع المصدر',
        'Draft not linked' => 'مسودة غير مرتبطة',
        'Ref' => 'المرجع',
        'Journal' => 'قيد اليومية',
        'Invoices' => 'الفواتير',
        'Net' => 'الصافي',
        'Total Interest' => 'إجمالي الفوائد',
        'Paid Total' => 'إجمالي المسدد',
        'Closing' => 'الإغلاق',
        'Closed' => 'مغلق',
        'Confirmed' => 'معتمد',
        'Collected' => 'محصل',
        'On Hold' => 'موقوف مؤقتا',
        'Expired' => 'منتهي',
        'Terminated' => 'منهي',
        'Planned' => 'مخطط',
        'Adjustment' => 'تسوية',
        'Change' => 'تغيير',
        'In' => 'وارد',
        'Out' => 'صادر',
        'Emergency' => 'طارئ',
        'Sick' => 'مرضي',
        'Agency' => 'وكالة',
        'Administration' => 'إدارة',
        'Marketing' => 'تسويق',
        'Maintenance' => 'صيانة',
        'Transportation' => 'نقل ومواصلات',
        'Communication' => 'اتصالات',
        'Other' => 'أخرى',
        'Manager' => 'مدير',
        'Monitoring' => 'متابعة',
        'Hospital' => 'مستشفى',
        'Imaging' => 'تصوير طبي',
        'Laparoscopy' => 'مناظير',
        'Orthopedic' => 'عظام',
        'Surgical Equipment' => 'معدات جراحية',
        'Capacity' => 'السعة',
    ];
}

function app_i18n_dictionary(): array
{
    static $dictionary = null;
    if ($dictionary === null) {
        $dictionary = array_replace(app_i18n_map(), app_i18n_professional_map(), app_i18n_accounting_coverage_map());
    }
    return $dictionary;
}

function app_ui_language(): string
{
    $lang = $_SESSION['ui_lang'] ?? ($_COOKIE['orion_lang'] ?? 'en');
    return in_array($lang, ['en', 'ar'], true) ? (string) $lang : 'en';
}

function app_normalize_ui_skin(string $skin): string
{
    $skin = strtolower(app_normalize_whitespace($skin));
    return in_array($skin, ['classic', 'finance'], true) ? $skin : 'finance';
}

function app_normalize_ui_mode(string $theme): string
{
    $theme = strtolower(app_normalize_whitespace($theme));
    return $theme === 'dark' ? 'dark' : 'light';
}

function app_default_ui_theme_preferences(): array
{
    static $preferences = null;
    if ($preferences !== null) {
        return $preferences;
    }

    $preferences = [
        'theme_code' => 'DEVELOP_LIGHT',
        'ui_skin' => 'finance',
        'color_mode' => 'light',
    ];

    try {
        $pdo = app_pdo();
        if (!app_table_exists($pdo, 'themes')) {
            return $preferences;
        }

        $stmt = $pdo->query(
            "SELECT theme_code, ui_skin, color_mode
             FROM themes
             WHERE is_active = 1
             ORDER BY is_default DESC, id ASC
             LIMIT 1"
        );
        $row = $stmt ? ($stmt->fetch() ?: null) : null;
        if (is_array($row)) {
            $preferences = [
                'theme_code' => app_normalize_whitespace((string) ($row['theme_code'] ?? 'DEVELOP_LIGHT')),
                'ui_skin' => app_normalize_ui_skin((string) ($row['ui_skin'] ?? 'finance')),
                'color_mode' => app_normalize_ui_mode((string) ($row['color_mode'] ?? 'light')),
            ];
        }
    } catch (Throwable) {
        // During first install the themes table may not exist yet.
    }

    return $preferences;
}

function app_ui_theme(): string
{
    $default = app_default_ui_theme_preferences();
    $theme = $_SESSION['ui_theme'] ?? ($_COOKIE['orion_theme'] ?? ($default['color_mode'] ?? 'light'));
    return app_normalize_ui_mode((string) $theme);
}

function app_ui_skin(): string
{
    $default = app_default_ui_theme_preferences();
    $skin = $_SESSION['ui_skin'] ?? ($_COOKIE['orion_skin'] ?? ($default['ui_skin'] ?? 'finance'));
    return app_normalize_ui_skin((string) $skin);
}

function app_ui_dir(): string
{
    return app_ui_language() === 'ar' ? 'rtl' : 'ltr';
}

function app_html_attrs(): string
{
    return 'lang="' . app_escape(app_ui_language()) . '" dir="' . app_escape(app_ui_dir()) . '" data-theme="' . app_escape(app_ui_theme()) . '" data-ui-skin="' . app_escape(app_ui_skin()) . '"';
}

function app_t(string $text): string
{
    if (app_ui_language() !== 'ar') {
        return $text;
    }

    $normalized = app_normalize_whitespace($text);
    if (preg_match('/^New\s+(.+)$/u', $normalized, $matches)) {
        return 'جديد ' . app_t((string) $matches[1]);
    }
    if (preg_match('/^Search\s+(.+)$/u', $normalized, $matches)) {
        return 'بحث ' . app_t((string) $matches[1]);
    }
    if (preg_match('/^Template:\s+(.+)$/u', $normalized, $matches)) {
        return 'القالب: ' . (string) $matches[1];
    }
    if (preg_match('/^Section\s+(\d+)$/u', $normalized, $matches)) {
        return 'بند ' . (string) $matches[1];
    }
    if (preg_match('/^(\d[\d,]*)\s+row\(s\)$/u', $normalized, $matches)) {
        return (string) $matches[1] . ' صف';
    }
    if (preg_match('/^(\d[\d,]*)\s+template\(s\)$/u', $normalized, $matches)) {
        return (string) $matches[1] . ' قالب';
    }
    if (preg_match('/^(\d[\d,]*)\s+entry\(s\), newest accounting dates first$/u', $normalized, $matches)) {
        return (string) $matches[1] . ' قيد، الأحدث حسب تاريخ القيد أولا';
    }
    if (preg_match('/^Difference\s+(.+)$/u', $normalized, $matches)) {
        return 'الفرق ' . (string) $matches[1];
    }
    if (preg_match('/^Unbalanced:\s+(.+)$/u', $normalized, $matches)) {
        return 'غير متوازن: ' . (string) $matches[1];
    }
    if (preg_match('/^Depreciation run\s+(.+)\s+created as draft\. Assets:\s+(.+), Amount:\s+(.+)$/u', $normalized, $matches)) {
        return 'تم إنشاء تشغيل الإهلاك ' . (string) $matches[1] . ' كمسودة. عدد الأصول: ' . (string) $matches[2] . '، المبلغ: ' . (string) $matches[3];
    }
    if (preg_match('/^Prepaid amortization created as draft\. Runs:\s+(.+), Skipped:\s+(.+), Amount:\s+(.+)$/u', $normalized, $matches)) {
        return 'تم إنشاء إطفاء المصروفات المقدمة كمسودة. القيود: ' . (string) $matches[1] . '، المتجاوزة: ' . (string) $matches[2] . '، المبلغ: ' . (string) $matches[3];
    }
    if (preg_match('/^Import completed:\s+(.+)\s+inserted,\s+(.+)\s+skipped,\s+(.+)\s+errors\.$/u', $normalized, $matches)) {
        return 'اكتمل الاستيراد: تم إدراج ' . (string) $matches[1] . '، تم تجاوز ' . (string) $matches[2] . '، أخطاء ' . (string) $matches[3] . '.';
    }
    if (preg_match('/^(.+)\s+record\(s\) deleted successfully\.$/u', $normalized, $matches)) {
        return 'تم حذف ' . (string) $matches[1] . ' سجل بنجاح.';
    }
    if (preg_match('/^Open\s+(.+)$/u', $normalized, $matches)) {
        return 'فتح ' . app_t((string) $matches[1]);
    }
    if (preg_match('/^Journal\s+(.+)$/u', $normalized, $matches)) {
        return 'قيد ' . (string) $matches[1];
    }

    return app_i18n_dictionary()[$normalized] ?? $text;
}

function app_localize_module_definition(array $definition): array
{
    if (isset($definition['title'])) {
        $definition['title'] = app_t((string) $definition['title']);
    }
    if (isset($definition['description'])) {
        $definition['description'] = app_t((string) $definition['description']);
    }
    foreach ((array) ($definition['fields'] ?? []) as $index => $field) {
        if (isset($field['label'])) {
            $definition['fields'][$index]['label'] = app_t((string) $field['label']);
        }
        if (isset($field['options']) && is_array($field['options'])) {
            foreach ($field['options'] as $key => $label) {
                $definition['fields'][$index]['options'][$key] = app_t((string) $label);
            }
        }
    }
    return $definition;
}

function app_translate_html_text(string $text): string
{
    $trimmed = app_normalize_whitespace(html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    if ($trimmed === '') {
        return $text;
    }

    $translated = app_t($trimmed);
    if ($translated === $trimmed) {
        return $text;
    }

    $prefix = '';
    $suffix = '';
    if (preg_match('/^\s*/u', $text, $startMatch)) {
        $prefix = $startMatch[0] ?? '';
    }
    if (preg_match('/\s*$/u', $text, $endMatch)) {
        $suffix = $endMatch[0] ?? '';
    }
    return $prefix . app_escape($translated) . $suffix;
}

function app_localize_html_chunk(string $html): string
{
    $html = preg_replace_callback(
        '/>([^<]+)</u',
        static fn (array $matches): string => '>' . app_translate_html_text($matches[1]) . '<',
        $html
    ) ?? $html;

    $html = preg_replace_callback(
        '/\b(placeholder|title|aria-label|alt)="([^"]*)"/u',
        static function (array $matches): string {
            $value = html_entity_decode($matches[2], ENT_QUOTES | ENT_HTML5, 'UTF-8');
            return $matches[1] . '="' . app_escape(app_t($value)) . '"';
        },
        $html
    ) ?? $html;

    return $html;
}

function app_localize_html_output(string $html): string
{
    if (app_ui_language() !== 'ar' || $html === '') {
        return $html;
    }

    $parts = preg_split('/(<(?:script|style)\b[^>]*>.*?<\/(?:script|style)>)/isu', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
    if (!is_array($parts)) {
        return app_localize_html_chunk($html);
    }

    foreach ($parts as $index => $part) {
        if ($part === '' || preg_match('/^<(?:script|style)\b/iu', $part)) {
            continue;
        }
        $parts[$index] = app_localize_html_chunk($part);
    }
    return implode('', $parts);
}

function app_bootstrap_ui_preferences(): void
{
    $lang = $_GET['lang'] ?? ($_COOKIE['orion_lang'] ?? ($_SESSION['ui_lang'] ?? 'en'));
    $lang = in_array($lang, ['en', 'ar'], true) ? (string) $lang : 'en';
    $_SESSION['ui_lang'] = $lang;

    $defaultTheme = app_default_ui_theme_preferences();
    $skin = $_GET['skin'] ?? ($_GET['ui_skin'] ?? ($_COOKIE['orion_skin'] ?? ($_SESSION['ui_skin'] ?? ($defaultTheme['ui_skin'] ?? 'finance'))));
    $skin = app_normalize_ui_skin((string) $skin);
    $_SESSION['ui_skin'] = $skin;

    $theme = $_GET['theme'] ?? ($_GET['ui_theme'] ?? ($_COOKIE['orion_theme'] ?? ($_SESSION['ui_theme'] ?? ($defaultTheme['color_mode'] ?? 'light'))));
    $theme = app_normalize_ui_mode((string) $theme);
    $_SESSION['ui_theme'] = $theme;

    if (!headers_sent()) {
        setcookie('orion_lang', $lang, time() + 31536000, '/');
        setcookie('orion_theme', $theme, time() + 31536000, '/');
        setcookie('orion_skin', $skin, time() + 31536000, '/');
    }

    static $bufferStarted = false;
    if (!$bufferStarted) {
        ob_start('app_localize_html_output');
        $bufferStarted = true;
    }
}

function app_ui_controls_html(string $placement = 'sidebar'): string
{
    $themeLabel = app_ui_theme() === 'dark' ? app_t('Light mode') : app_t('Dark mode');
    $languageLabel = app_ui_language() === 'ar' ? app_t('English') : app_t('Arabic');
    $class = $placement === 'auth' ? 'ui-switcher ui-switcher--auth screen-only' : 'ui-switcher screen-only';

    return '<div class="' . app_escape($class) . '">' .
        '<button id="themeToggle" class="ui-toggle ui-toggle--theme" type="button" data-ui-theme-toggle aria-label="' . app_escape(app_t('Theme')) . '">' .
            '<span class="ui-toggle__icon">DM</span><span class="ui-toggle__text">' . app_escape($themeLabel) . '</span>' .
        '</button>' .
        '<button id="languageToggle" class="ui-toggle ui-toggle--lang" type="button" data-ui-lang-toggle aria-label="' . app_escape(app_t('Language')) . '">' .
            '<span class="ui-toggle__icon">AR</span><span class="ui-toggle__text">' . app_escape($languageLabel) . '</span>' .
        '</button>' .
    '</div>';
}

function app_redirect(string $path): never
{
    header('Location: ' . $path);
    exit;
}

function app_flash(string $key, ?string $value = null): ?string
{
    if (!isset($_SESSION['flash'])) {
        $_SESSION['flash'] = [];
    }

    if ($value === null) {
        $message = $_SESSION['flash'][$key] ?? null;
        unset($_SESSION['flash'][$key]);
        return $message;
    }

    $_SESSION['flash'][$key] = $value;
    return null;
}

function app_csrf_token(): string
{
    if (empty($_SESSION['_csrf_token'])) {
        $_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
    }

    return (string) $_SESSION['_csrf_token'];
}

function app_csrf_field(): string
{
    return '<input type="hidden" name="_token" value="' . app_escape(app_csrf_token()) . '">';
}

function app_verify_csrf(): void
{
    $submitted = (string) ($_POST['_token'] ?? '');
    if ($submitted === '' || !hash_equals(app_csrf_token(), $submitted)) {
        throw new RuntimeException('CSRF token validation failed.');
    }
}

function app_normalize_whitespace(string $text): string
{
    return preg_replace('/\s+/u', ' ', trim($text)) ?? trim($text);
}

function app_number(mixed $value): float
{
    if ($value === null) {
        return 0.0;
    }

    $text = trim((string) $value);
    if ($text === '') {
        return 0.0;
    }

    $text = str_replace(["\xc2\xa0", ' ', ','], '', $text);
    $text = preg_replace('/[^\d\.\-\+]/u', '', $text) ?? $text;
    if ($text === '' || $text === '-' || $text === '+') {
        return 0.0;
    }

    return (float) $text;
}

function app_integer(mixed $value): int
{
    return (int) round(app_number($value));
}

function app_money_format(mixed $value): string
{
    $number = app_number($value);
    $formatted = number_format($number, 2, '.', ',');
    $formatted = rtrim(rtrim($formatted, '0'), '.');
    return $formatted === '-0' ? '0' : $formatted;
}

function app_date_format(?string $value): string
{
    if ($value === null || trim($value) === '') {
        return 'N/A';
    }

    $timestamp = strtotime($value);
    if ($timestamp === false) {
        return app_escape($value);
    }

    return date('Y-m-d', $timestamp);
}

function app_xlsx_xml_escape(mixed $value): string
{
    return htmlspecialchars((string) $value, ENT_QUOTES | ENT_XML1 | ENT_SUBSTITUTE, 'UTF-8');
}

function app_xlsx_column_name(int $index): string
{
    $index = max(1, $index);
    $name = '';
    while ($index > 0) {
        $index--;
        $name = chr(65 + ($index % 26)) . $name;
        $index = intdiv($index, 26);
    }
    return $name;
}

function app_xlsx_sheet_name(string $name): string
{
    $name = preg_replace('/[\[\]\:\*\?\/\\\\]/', ' ', $name) ?: 'Sheet';
    $name = trim(preg_replace('/\s+/', ' ', $name) ?: 'Sheet');
    $name = $name !== '' ? $name : 'Sheet';
    return function_exists('mb_substr') ? mb_substr($name, 0, 31, 'UTF-8') : substr($name, 0, 31);
}

function app_xlsx_safe_filename(string $name): string
{
    $name = preg_replace('/[^\pL\pN\-_\.]+/u', '-', trim($name)) ?: 'export';
    $name = trim($name, '-_.');
    if ($name === '') {
        $name = 'export';
    }
    return str_ends_with(strtolower($name), '.xlsx') ? $name : ($name . '.xlsx');
}

function app_xlsx_row(array $cells, string $style = ''): array
{
    return ['_cells' => $cells, '_style' => $style];
}

function app_xlsx_row_cells(array $row): array
{
    if (array_key_exists('_cells', $row) && is_array($row['_cells'])) {
        return $row['_cells'];
    }
    return $row;
}

function app_xlsx_row_style(array $row): string
{
    return (string) ($row['_style'] ?? '');
}

function app_xlsx_cell_xml(mixed $value, string $cellRef, int $styleIndex = 0): string
{
    $styleAttr = $styleIndex > 0 ? ' s="' . $styleIndex . '"' : '';
    if (is_int($value) || is_float($value)) {
        return '<c r="' . $cellRef . '"' . $styleAttr . '><v>' . app_xlsx_xml_escape((string) $value) . '</v></c>';
    }

    if (is_numeric($value) && !is_string($value)) {
        return '<c r="' . $cellRef . '"' . $styleAttr . '><v>' . app_xlsx_xml_escape((string) $value) . '</v></c>';
    }

    $text = app_xlsx_xml_escape((string) $value);
    return '<c r="' . $cellRef . '" t="inlineStr"' . $styleAttr . '><is><t>' . $text . '</t></is></c>';
}

function app_xlsx_sheet_xml(array $rows): string
{
    $styleMap = [
        'title' => 1,
        'header' => 2,
        'total' => 3,
    ];
    $xmlRows = [];
    $rowIndex = 1;
    foreach ($rows as $row) {
        $row = is_array($row) ? $row : [(string) $row];
        $cells = app_xlsx_row_cells($row);
        $styleIndex = $styleMap[app_xlsx_row_style($row)] ?? 0;
        $cellXml = [];
        $columnIndex = 1;
        foreach ($cells as $cellValue) {
            $cellRef = app_xlsx_column_name($columnIndex) . $rowIndex;
            $cellXml[] = app_xlsx_cell_xml($cellValue, $cellRef, $styleIndex);
            $columnIndex++;
        }
        $xmlRows[] = '<row r="' . $rowIndex . '">' . implode('', $cellXml) . '</row>';
        $rowIndex++;
    }

    return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
        . 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
        . '<sheetViews><sheetView workbookViewId="0"/></sheetViews>'
        . '<sheetFormatPr defaultRowHeight="18"/>'
        . '<sheetData>' . implode('', $xmlRows) . '</sheetData>'
        . '</worksheet>';
}

function app_export_xlsx(string $filename, array $sheets): void
{
    if (!$sheets) {
        $sheets = [['title' => 'Export', 'rows' => [app_xlsx_row(['No data available'], 'title')]]];
    }
    if (!class_exists('ZipArchive')) {
        throw new RuntimeException('PHP Zip extension is required to export XLSX files.');
    }

    $tmpPath = tempnam(sys_get_temp_dir(), 'orion_xlsx_');
    if ($tmpPath === false) {
        throw new RuntimeException('Unable to create temporary XLSX file.');
    }

    $zip = new ZipArchive();
    if ($zip->open($tmpPath, ZipArchive::OVERWRITE) !== true) {
        @unlink($tmpPath);
        throw new RuntimeException('Unable to create XLSX archive.');
    }

    $sheetXml = [];
    $sheetRels = [];
    foreach (array_values($sheets) as $index => $sheet) {
        $sheetNumber = $index + 1;
        $title = app_xlsx_sheet_name((string) ($sheet['title'] ?? ('Sheet ' . $sheetNumber)));
        $rows = is_array($sheet['rows'] ?? null) ? $sheet['rows'] : [];
        $sheetXml[] = '<sheet name="' . app_xlsx_xml_escape($title) . '" sheetId="' . $sheetNumber . '" r:id="rId' . $sheetNumber . '"/>';
        $sheetRels[] = '<Relationship Id="rId' . $sheetNumber . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet' . $sheetNumber . '.xml"/>';
        $zip->addFromString('xl/worksheets/sheet' . $sheetNumber . '.xml', app_xlsx_sheet_xml($rows));
    }
    $stylesRelId = count($sheets) + 1;
    $sheetRels[] = '<Relationship Id="rId' . $stylesRelId . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';

    $zip->addFromString('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
        . '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
        . '<Default Extension="xml" ContentType="application/xml"/>'
        . '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
        . '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
        . implode('', array_map(static fn (int $i): string => '<Override PartName="/xl/worksheets/sheet' . $i . '.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>', range(1, count($sheets))))
        . '</Types>');
    $zip->addFromString('_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
        . '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
        . '</Relationships>');
    $zip->addFromString('xl/workbook.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
        . 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
        . '<sheets>' . implode('', $sheetXml) . '</sheets></workbook>');
    $zip->addFromString('xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
        . implode('', $sheetRels)
        . '</Relationships>');
    $zip->addFromString('xl/styles.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
        . '<fonts count="3"><font><sz val="11"/><color theme="1"/><name val="Calibri"/></font><font><b/><sz val="14"/><color rgb="FFFFFFFF"/><name val="Calibri"/></font><font><b/><sz val="11"/><color rgb="FF0F172A"/><name val="Calibri"/></font></fonts>'
        . '<fills count="4"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF146C94"/><bgColor indexed="64"/></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="FFEAF4FF"/><bgColor indexed="64"/></patternFill></fill></fills>'
        . '<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"><color rgb="FFD8E3EE"/></left><right style="thin"><color rgb="FFD8E3EE"/></right><top style="thin"><color rgb="FFD8E3EE"/></top><bottom style="thin"><color rgb="FFD8E3EE"/></bottom><diagonal/></border></borders>'
        . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
        . '<cellXfs count="4"><xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0"/><xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1"/><xf numFmtId="0" fontId="2" fillId="3" borderId="1" xfId="0" applyFont="1" applyFill="1"/><xf numFmtId="0" fontId="2" fillId="3" borderId="1" xfId="0" applyFont="1" applyFill="1"/></cellXfs>'
        . '</styleSheet>');
    $zip->close();

    $downloadName = app_xlsx_safe_filename($filename);
    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"');
    header('Content-Length: ' . (string) filesize($tmpPath));
    header('Cache-Control: no-store, no-cache, must-revalidate');
    readfile($tmpPath);
    @unlink($tmpPath);
    exit;
}

function app_current_user(): ?array
{
    return $_SESSION['user'] ?? null;
}

function app_is_logged_in(): bool
{
    return !empty($_SESSION['user']['id']);
}

function app_is_admin(): bool
{
    return (string) (app_current_user()['role'] ?? '') === 'admin';
}

function app_current_role_code(): string
{
    $role = strtolower(app_normalize_whitespace((string) (app_current_user()['role'] ?? 'accountant')));
    $role = preg_replace('/[^a-z0-9_]+/', '_', $role) ?? 'accountant';
    $role = trim($role, '_');
    return $role !== '' ? $role : 'accountant';
}

function app_role_exists(string $roleCode, bool $activeOnly = true): bool
{
    $roleCode = app_current_role_normalize($roleCode);
    if ($roleCode === '') {
        return false;
    }
    if (in_array($roleCode, ['admin', 'accountant', 'auditor'], true)) {
        return true;
    }

    try {
        $sql = 'SELECT 1 FROM roles WHERE role_code = :role_code';
        if ($activeOnly) {
            $sql .= ' AND is_active = 1';
        }
        $sql .= ' LIMIT 1';
        $stmt = app_pdo()->prepare($sql);
        $stmt->execute(['role_code' => $roleCode]);
        return (bool) $stmt->fetchColumn();
    } catch (Throwable) {
        return false;
    }
}

function app_current_role_normalize(string $roleCode): string
{
    $roleCode = strtolower(app_normalize_whitespace($roleCode));
    $roleCode = preg_replace('/[^a-z0-9_]+/', '_', $roleCode) ?? '';
    return trim($roleCode, '_');
}

function app_permission_module_options(): array
{
    $options = [];
    foreach (app_module_definitions() as $slug => $definition) {
        $title = app_normalize_whitespace((string) ($definition['title'] ?? $slug));
        $title = $title !== '' ? $title : (string) $slug;
        $options[(string) $slug] = app_ui_language() === 'ar'
            ? app_t($title)
            : ((string) $slug . ' - ' . $title);
    }
    ksort($options, SORT_NATURAL);
    return $options;
}

function app_role_default_permission(string $roleCode, string $moduleSlug): array
{
    $roleCode = app_current_role_normalize($roleCode);
    $moduleSlug = app_normalize_whitespace($moduleSlug);
    $definitions = app_module_definitions();
    $definition = $definitions[$moduleSlug] ?? [];
    $isAdminOnly = !empty($definition['admin_only']);

    $base = [
        'can_access' => 0,
        'can_create' => 0,
        'can_edit' => 0,
        'can_delete' => 0,
        'can_import' => 0,
        'can_post' => 0,
    ];

    if ($roleCode === 'admin') {
        return array_fill_keys(array_keys($base), 1);
    }

    if ($isAdminOnly) {
        return $base;
    }

    if ($roleCode === 'auditor') {
        $base['can_access'] = 1;
        return $base;
    }

    if ($roleCode !== 'accountant') {
        return $base;
    }

    $base = array_fill_keys(array_keys($base), 1);
    $base['can_delete'] = 0;
    if (!in_array($moduleSlug, ['journal_entries', 'sales_invoices', 'purchase_bills', 'expenses', 'sales_returns', 'purchase_returns', 'expense_returns', 'payments', 'student_invoices', 'student_receipts', 'student_refunds', 'prepaid_expenses', 'prepaid_amortization_runs', 'depreciation_runs', 'ifrs16_leases', 'ifrs16_lease_schedules', 'pos_orders', 'pos_sessions', 'pos_cash_movements', 'excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'], true)) {
        $base['can_post'] = 0;
    }
    return $base;
}

function app_role_permission(string $moduleSlug, string $capability = 'access', ?string $roleCode = null): bool
{
    $moduleSlug = app_normalize_whitespace($moduleSlug);
    $capability = strtolower(app_normalize_whitespace($capability));
    $permissionColumn = match ($capability) {
        'create' => 'can_create',
        'edit', 'update' => 'can_edit',
        'delete', 'remove' => 'can_delete',
        'import' => 'can_import',
        'post', 'posting', 'workflow' => 'can_post',
        default => 'can_access',
    };

    $roleCode = $roleCode === null ? app_current_role_code() : app_current_role_normalize($roleCode);
    if ($roleCode === 'admin' || app_is_admin()) {
        return true;
    }

    static $cache = [];
    $cacheKey = $roleCode . '|' . $moduleSlug;
    if (!array_key_exists($cacheKey, $cache)) {
        $permission = app_role_default_permission($roleCode, $moduleSlug);
        try {
            $stmt = app_pdo()->prepare(
                'SELECT can_access, can_create, can_edit, can_delete, can_import, can_post
                 FROM role_permissions
                 WHERE role_code = :role_code
                   AND module_slug = :module_slug
                 LIMIT 1'
            );
            $stmt->execute([
                'role_code' => $roleCode,
                'module_slug' => $moduleSlug,
            ]);
            $row = $stmt->fetch();
            if ($row) {
                foreach (array_keys($permission) as $key) {
                    $permission[$key] = (int) ($row[$key] ?? 0);
                }
            }
        } catch (Throwable) {
            // During first bootstrap the permission tables may not exist yet.
        }

        if (empty($permission['can_access'])) {
            foreach (array_keys($permission) as $key) {
                $permission[$key] = 0;
            }
        }
        $cache[$cacheKey] = $permission;
    }

    return !empty($cache[$cacheKey][$permissionColumn]);
}

function app_current_role_can(string $moduleSlug, string $capability = 'access'): bool
{
    return app_role_permission($moduleSlug, $capability);
}

function app_enforce_current_role_can(string $moduleSlug, string $capability, string $message = ''): void
{
    if (app_current_role_can($moduleSlug, $capability)) {
        return;
    }
    if ($message === '') {
        $message = 'You do not have permission to ' . $capability . ' in this module.';
    }
    throw new RuntimeException($message);
}

function app_roles_list(bool $activeOnly = true): array
{
    try {
        $sql = 'SELECT * FROM roles';
        if ($activeOnly) {
            $sql .= ' WHERE is_active = 1';
        }
        $sql .= ' ORDER BY is_system DESC, role_name ASC, role_code ASC';
        return app_pdo()->query($sql)->fetchAll() ?: [];
    } catch (Throwable) {
        return [
            ['role_code' => 'admin', 'role_name' => 'Administrator', 'is_system' => 1, 'is_active' => 1],
            ['role_code' => 'accountant', 'role_name' => 'Accountant', 'is_system' => 1, 'is_active' => 1],
            ['role_code' => 'auditor', 'role_name' => 'Auditor', 'is_system' => 1, 'is_active' => 1],
        ];
    }
}

function app_role_permission_simple_state(string $roleCode): array
{
    $roleCode = app_current_role_normalize($roleCode);
    $moduleSlugs = array_keys(app_module_definitions());
    $allowed = [];
    $canEditAll = true;
    $canPostAll = true;
    $hasAllowed = false;

    foreach ($moduleSlugs as $moduleSlug) {
        $permission = app_role_default_permission($roleCode, (string) $moduleSlug);
        try {
            $stmt = app_pdo()->prepare(
                'SELECT can_access, can_create, can_edit, can_delete, can_import, can_post
                 FROM role_permissions
                 WHERE role_code = :role_code
                   AND module_slug = :module_slug
                 LIMIT 1'
            );
            $stmt->execute([
                'role_code' => $roleCode,
                'module_slug' => (string) $moduleSlug,
            ]);
            $row = $stmt->fetch();
            if ($row) {
                foreach (array_keys($permission) as $key) {
                    $permission[$key] = (int) ($row[$key] ?? 0);
                }
            }
        } catch (Throwable) {
            // Keep default permissions during bootstrap.
        }

        if (!empty($permission['can_access'])) {
            $hasAllowed = true;
            $allowed[] = (string) $moduleSlug;
            foreach (['can_create', 'can_edit', 'can_delete', 'can_import'] as $editKey) {
                if (empty($permission[$editKey])) {
                    $canEditAll = false;
                    break;
                }
            }
            if (empty($permission['can_post'])) {
                $canPostAll = false;
            }
        }
    }

    if (!$hasAllowed) {
        $canEditAll = false;
        $canPostAll = false;
    }

    return [
        'allowed_modules' => $allowed,
        'can_edit_all' => $canEditAll,
        'can_post_all' => $canPostAll,
    ];
}

function app_save_simple_role_permissions(string $roleCode, array $allowedModules, bool $canEditAll, bool $canPostAll): void
{
    $roleCode = app_current_role_normalize($roleCode);
    if (!app_role_exists($roleCode, true)) {
        throw new RuntimeException('Select a valid active role.');
    }

    $definitions = app_module_definitions();
    $allowedMap = [];
    foreach ($allowedModules as $moduleSlug) {
        $moduleSlug = app_normalize_whitespace((string) $moduleSlug);
        if ($moduleSlug !== '' && array_key_exists($moduleSlug, $definitions)) {
            $allowedMap[$moduleSlug] = true;
        }
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'INSERT INTO role_permissions
         (role_code, module_slug, can_access, can_create, can_edit, can_delete, can_import, can_post)
         VALUES
         (:role_code, :module_slug, :can_access, :can_create, :can_edit, :can_delete, :can_import, :can_post)
         ON DUPLICATE KEY UPDATE
            can_access = VALUES(can_access),
            can_create = VALUES(can_create),
            can_edit = VALUES(can_edit),
            can_delete = VALUES(can_delete),
            can_import = VALUES(can_import),
            can_post = VALUES(can_post)'
    );

    foreach (array_keys($definitions) as $moduleSlug) {
        $isAllowed = isset($allowedMap[(string) $moduleSlug]);
        $editValue = $isAllowed && $canEditAll ? 1 : 0;
        $stmt->execute([
            'role_code' => $roleCode,
            'module_slug' => (string) $moduleSlug,
            'can_access' => $isAllowed ? 1 : 0,
            'can_create' => $editValue,
            'can_edit' => $editValue,
            'can_delete' => $editValue,
            'can_import' => $editValue,
            'can_post' => $isAllowed && $canPostAll ? 1 : 0,
        ]);
    }
}

function app_current_username(): string
{
    return (string) (app_current_user()['username'] ?? 'admin');
}

function app_current_company_id(): ?int
{
    $value = app_current_user()['company_id'] ?? null;
    return $value === null || $value === '' ? null : (int) $value;
}

function app_current_company_name(): ?string
{
    $name = app_current_user()['company_name'] ?? null;
    $name = is_string($name) ? trim($name) : '';
    return $name !== '' ? $name : null;
}

function app_current_company_logo(): ?string
{
    $logo = app_current_user()['company_logo'] ?? null;
    $logo = is_string($logo) ? trim($logo) : '';
    return $logo !== '' ? $logo : null;
}

function app_user_accessible_companies(int $userId): array
{
    if ($userId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $userStmt = $pdo->prepare('SELECT id, role, company_id FROM users WHERE id = :id AND is_active = 1 LIMIT 1');
    $userStmt->execute(['id' => $userId]);
    $user = $userStmt->fetch();
    if (!$user) {
        return [];
    }

    if ((string) ($user['role'] ?? '') === 'admin') {
        return app_companies(true);
    }

    $stmt = $pdo->prepare(
        'SELECT DISTINCT c.*
         FROM companies c
         LEFT JOIN user_company_access uca
           ON uca.company_id = c.id
          AND uca.user_id = :access_user_id
         LEFT JOIN users u
           ON u.id = :fallback_user_id
         WHERE c.is_active = 1
           AND (uca.user_id IS NOT NULL OR c.id = u.company_id)
         ORDER BY c.name ASC'
    );
    $stmt->execute([
        'access_user_id' => $userId,
        'fallback_user_id' => $userId,
    ]);
    return $stmt->fetchAll() ?: [];
}

function app_current_user_company_options(): array
{
    $userId = (int) (app_current_user()['id'] ?? 0);
    return $userId > 0 ? app_user_accessible_companies($userId) : [];
}

function app_user_can_access_company(int $userId, int $companyId): bool
{
    if ($userId <= 0 || $companyId <= 0) {
        return false;
    }

    foreach (app_user_accessible_companies($userId) as $company) {
        if ((int) ($company['id'] ?? 0) === $companyId) {
            return true;
        }
    }
    return false;
}

function app_current_user_can_access_company(int $companyId): bool
{
    $userId = (int) (app_current_user()['id'] ?? 0);
    return app_user_can_access_company($userId, $companyId);
}

function app_user_company_ids(int $userId): array
{
    if ($userId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT DISTINCT company_id
         FROM user_company_access
         WHERE user_id = :user_id
         ORDER BY company_id ASC'
    );
    $stmt->execute(['user_id' => $userId]);
    $ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []);

    if (!$ids) {
        $fallbackStmt = $pdo->prepare('SELECT company_id FROM users WHERE id = :user_id LIMIT 1');
        $fallbackStmt->execute(['user_id' => $userId]);
        $fallbackId = (int) ($fallbackStmt->fetchColumn() ?: 0);
        if ($fallbackId > 0) {
            $ids[] = $fallbackId;
        }
    }

    return array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0)));
}

function app_user_company_names(int $userId): string
{
    $names = [];
    foreach (app_user_accessible_companies($userId) as $company) {
        $name = app_normalize_whitespace((string) ($company['name'] ?? ''));
        if ($name !== '') {
            $names[] = $name;
        }
    }
    return $names ? implode(', ', $names) : app_t('No Company');
}

function app_sync_user_company_access(int $userId, array $companyIds): void
{
    if ($userId <= 0) {
        throw new RuntimeException('Invalid user identifier.');
    }

    $cleanIds = [];
    foreach ($companyIds as $companyId) {
        $companyId = (int) $companyId;
        if ($companyId > 0 && app_company_by_id($companyId)) {
            $cleanIds[$companyId] = $companyId;
        }
    }
    $cleanIds = array_values($cleanIds);

    $pdo = app_pdo();
    $startedTransaction = false;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        $delete = $pdo->prepare('DELETE FROM user_company_access WHERE user_id = :user_id');
        $delete->execute(['user_id' => $userId]);

        if ($cleanIds) {
            $insert = $pdo->prepare(
                'INSERT IGNORE INTO user_company_access (user_id, company_id)
                 VALUES (:user_id, :company_id)'
            );
            foreach ($cleanIds as $companyId) {
                $insert->execute([
                    'user_id' => $userId,
                    'company_id' => $companyId,
                ]);
            }
        }

        $primaryCompanyId = $cleanIds[0] ?? null;
        $update = $pdo->prepare('UPDATE users SET company_id = :company_id WHERE id = :user_id');
        $update->execute([
            'company_id' => $primaryCompanyId,
            'user_id' => $userId,
        ]);

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_delete_user_company_access(int $userId): void
{
    if ($userId <= 0) {
        return;
    }

    $stmt = app_pdo()->prepare('DELETE FROM user_company_access WHERE user_id = :user_id');
    $stmt->execute(['user_id' => $userId]);
}

function app_refresh_session_company(?int $companyId): void
{
    if ($companyId === null || $companyId <= 0) {
        unset($_SESSION['context_company_id']);
        if (isset($_SESSION['user'])) {
            $_SESSION['user']['company_id'] = null;
            $_SESSION['user']['company_name'] = '';
            $_SESSION['user']['company_logo'] = '';
        }
        return;
    }

    $company = app_company_by_id($companyId);
    if (!$company || (int) ($company['is_active'] ?? 0) !== 1) {
        throw new RuntimeException('Selected company is not available.');
    }

    $_SESSION['context_company_id'] = $companyId;
    if (isset($_SESSION['user'])) {
        $_SESSION['user']['company_id'] = $companyId;
        $_SESSION['user']['company_name'] = (string) ($company['name'] ?? '');
        $_SESSION['user']['company_logo'] = (string) ($company['logo_path'] ?? '');
    }
}

function app_login_user(array $user): void
{
    session_regenerate_id(true);
    $_SESSION['user'] = [
        'id' => (int) $user['id'],
        'username' => (string) $user['username'],
        'full_name' => (string) ($user['full_name'] ?? $user['username']),
        'role' => (string) ($user['role'] ?? 'accountant'),
        'company_id' => null,
        'company_name' => '',
        'company_logo' => '',
        'available_companies_count' => 0,
    ];
    unset($_SESSION['context_company_id']);

    $companies = app_current_user_company_options();
    $_SESSION['user']['available_companies_count'] = count($companies);
    if (count($companies) === 1) {
        app_set_context_company_id((int) ($companies[0]['id'] ?? 0));
    }
}

function app_logout_user(): void
{
    $_SESSION = [];
    if (session_status() === PHP_SESSION_ACTIVE) {
        session_destroy();
    }
}

function app_require_login(): void
{
    if (!app_is_logged_in()) {
        app_redirect('login.php');
    }

    $script = basename((string) ($_SERVER['SCRIPT_NAME'] ?? ''));
    if ($script !== 'select_company.php' && app_login_needs_company_selection()) {
        $requestedCompanyId = (int) ($_REQUEST['company_id'] ?? 0);
        if ($requestedCompanyId > 0 && app_current_user_can_access_company($requestedCompanyId)) {
            app_set_context_company_id($requestedCompanyId);
            return;
        }

        app_redirect('select_company.php');
    }
}

function app_context_company_id(): ?int
{
    $sessionId = $_SESSION['context_company_id'] ?? null;
    if ($sessionId !== null && $sessionId !== '') {
        return (int) $sessionId;
    }

    return app_current_company_id();
}

function app_set_context_company_id(?int $companyId): void
{
    if ($companyId === null || $companyId <= 0) {
        app_refresh_session_company(null);
        return;
    }

    app_refresh_session_company($companyId);
}

function app_login_needs_company_selection(): bool
{
    if (!app_is_logged_in() || app_bootstrap_error() !== null) {
        return false;
    }

    $companies = app_current_user_company_options();
    $_SESSION['user']['available_companies_count'] = count($companies);
    if (!$companies) {
        return false;
    }

    $selectedCompanyId = app_context_company_id();
    if (count($companies) === 1) {
        $onlyCompanyId = (int) ($companies[0]['id'] ?? 0);
        if ($onlyCompanyId > 0 && ($selectedCompanyId === null || !app_current_user_can_access_company($selectedCompanyId))) {
            app_set_context_company_id($onlyCompanyId);
        }
        return false;
    }

    return $selectedCompanyId === null || !app_current_user_can_access_company($selectedCompanyId);
}

function app_effective_company_id(?int $requestedCompanyId = null, bool $required = true): ?int
{
    $candidateIds = [];
    foreach ([$requestedCompanyId, app_context_company_id(), app_current_company_id()] as $candidateId) {
        $candidateId = (int) ($candidateId ?? 0);
        if ($candidateId > 0) {
            $candidateIds[$candidateId] = $candidateId;
        }
    }

    foreach ($candidateIds as $companyId) {
        if (app_current_user_can_access_company($companyId)) {
            app_set_context_company_id($companyId);
            return $companyId;
        }
    }

    $companies = app_current_user_company_options();
    if (count($companies) === 1) {
        $companyId = (int) ($companies[0]['id'] ?? 0);
        if ($companyId > 0) {
            app_set_context_company_id($companyId);
            return $companyId;
        }
    }

    if (!$required) {
        return null;
    }

    if (count($companies) > 1) {
        throw new RuntimeException('Please select a company first.');
    }

    if (app_is_logged_in()) {
        throw new RuntimeException('Company context is required.');
    }

    throw new RuntimeException('User company context is missing.');
}

function app_bootstrap_error(): ?string
{
    return $GLOBALS['app_bootstrap_error'] ?? null;
}

function app_identifier(string $value): bool
{
    return (bool) preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $value);
}

function app_pdo(bool $refresh = false): PDO
{
    static $pdo = null;

    if ($pdo !== null && !$refresh) {
        return $pdo;
    }

    $config = app_config();
    $charset = $config['db_charset'] ?? 'utf8mb4';
    $host = $config['db_host'] ?? '127.0.0.1';
    $port = (int) ($config['db_port'] ?? 3306);
    $dbName = $config['db_name'] ?? 'orion_accounts_erp';
    $dbUser = $config['db_user'] ?? 'root';
    $dbPass = $config['db_pass'] ?? '';

    $serverDsn = sprintf('mysql:host=%s;port=%d;charset=%s', $host, $port, $charset);
    $server = new PDO($serverDsn, $dbUser, $dbPass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    $server->exec(sprintf(
        'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET %s COLLATE utf8mb4_unicode_ci',
        str_replace('`', '``', $dbName),
        $charset
    ));

    $pdoDsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s', $host, $port, $dbName, $charset);
    $pdo = new PDO($pdoDsn, $dbUser, $dbPass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    return $pdo;
}

function app_database_exists(PDO $pdo, string $table): bool
{
    $stmt = $pdo->prepare(
        'SELECT 1
         FROM information_schema.tables
         WHERE table_schema = DATABASE()
           AND table_name = :table
         LIMIT 1'
    );
    $stmt->execute(['table' => $table]);
    return (bool) $stmt->fetchColumn();
}

function app_migrate(): void
{
    $pdo = app_pdo();

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS companies (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(190) NOT NULL UNIQUE,
    legal_name VARCHAR(220) NOT NULL DEFAULT '',
    logo_path VARCHAR(255) NOT NULL DEFAULT 'assets/logo.svg',
    currency_code VARCHAR(10) NOT NULL DEFAULT 'SAR',
    fiscal_year_start VARCHAR(10) NOT NULL DEFAULT '01-01',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS users (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(80) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(150) NOT NULL DEFAULT '',
    role VARCHAR(30) NOT NULL DEFAULT 'accountant',
    company_id INT UNSIGNED NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_users_company_id (company_id),
    INDEX idx_users_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS roles (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    role_code VARCHAR(60) NOT NULL UNIQUE,
    role_name VARCHAR(150) NOT NULL,
    description TEXT NULL,
    is_system TINYINT(1) NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_roles_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS role_permissions (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    role_code VARCHAR(60) NOT NULL,
    module_slug VARCHAR(120) NOT NULL,
    can_access TINYINT(1) NOT NULL DEFAULT 0,
    can_create TINYINT(1) NOT NULL DEFAULT 0,
    can_edit TINYINT(1) NOT NULL DEFAULT 0,
    can_delete TINYINT(1) NOT NULL DEFAULT 0,
    can_import TINYINT(1) NOT NULL DEFAULT 0,
    can_post TINYINT(1) NOT NULL DEFAULT 0,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_role_permission_module (role_code, module_slug),
    INDEX idx_role_permissions_role (role_code),
    INDEX idx_role_permissions_module (module_slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS themes (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    theme_code VARCHAR(60) NOT NULL,
    theme_name VARCHAR(150) NOT NULL,
    ui_skin VARCHAR(30) NOT NULL DEFAULT 'classic',
    color_mode VARCHAR(20) NOT NULL DEFAULT 'light',
    is_default TINYINT(1) NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_themes_code (theme_code),
    INDEX idx_themes_default (is_default),
    INDEX idx_themes_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS user_company_access (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    company_id INT UNSIGNED NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_user_company_access (user_id, company_id),
    INDEX idx_user_company_access_user (user_id),
    INDEX idx_user_company_access_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS chart_of_accounts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    account_code VARCHAR(60) NOT NULL,
    account_name VARCHAR(190) NOT NULL,
    account_type VARCHAR(40) NOT NULL,
    parent_code VARCHAR(60) NOT NULL DEFAULT '',
    opening_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    financial_statement_group_code VARCHAR(60) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_coa_company_code (company_id, account_code),
    INDEX idx_coa_company (company_id),
    INDEX idx_coa_type (account_type),
    INDEX idx_coa_statement_group (financial_statement_group_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS contacts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    contact_type VARCHAR(20) NOT NULL,
    contact_code VARCHAR(60) NOT NULL,
    contact_name VARCHAR(190) NOT NULL,
    tax_number VARCHAR(80) NOT NULL DEFAULT '',
    withholding_tax_name VARCHAR(120) NOT NULL DEFAULT '',
    withholding_tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    phone VARCHAR(80) NOT NULL DEFAULT '',
    email VARCHAR(180) NOT NULL DEFAULT '',
    address TEXT NULL,
    opening_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_contacts_company_code (company_id, contact_code),
    INDEX idx_contacts_company (company_id),
    INDEX idx_contacts_type (contact_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS tax_rates (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    tax_name VARCHAR(120) NOT NULL,
    rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    applies_to VARCHAR(20) NOT NULL DEFAULT 'both',
    tax_account_code VARCHAR(60) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_tax_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS products (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    sku VARCHAR(80) NOT NULL,
    product_name VARCHAR(190) NOT NULL,
    category VARCHAR(120) NOT NULL DEFAULT '',
    unit_name VARCHAR(60) NOT NULL DEFAULT 'EA',
    sale_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    purchase_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    qty_on_hand DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_products_company_sku (company_id, sku),
    INDEX idx_products_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS bank_accounts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    account_name VARCHAR(160) NOT NULL,
    bank_name VARCHAR(160) NOT NULL DEFAULT '',
    gl_account_code VARCHAR(60) NOT NULL DEFAULT '',
    iban VARCHAR(80) NOT NULL DEFAULT '',
    account_number VARCHAR(80) NOT NULL DEFAULT '',
    opening_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    current_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_bank_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS cash_accounts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    cash_name VARCHAR(160) NOT NULL,
    gl_account_code VARCHAR(60) NOT NULL DEFAULT '',
    custodian_name VARCHAR(160) NOT NULL DEFAULT '',
    location_name VARCHAR(160) NOT NULL DEFAULT '',
    opening_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    current_balance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_cash_company_name (company_id, cash_name),
    INDEX idx_cash_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_registers (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    register_code VARCHAR(60) NOT NULL,
    register_name VARCHAR(190) NOT NULL,
    cash_account_name VARCHAR(160) NOT NULL DEFAULT '',
    default_bank_account_name VARCHAR(160) NOT NULL DEFAULT '',
    default_customer_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    sales_revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    price_includes_tax TINYINT(1) NOT NULL DEFAULT 0,
    allow_negative_stock TINYINT(1) NOT NULL DEFAULT 0,
    receipt_footer TEXT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_pos_register_company_code (company_id, register_code),
    INDEX idx_pos_register_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_sessions (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    register_id BIGINT UNSIGNED NOT NULL,
    session_number VARCHAR(60) NOT NULL,
    opened_at DATETIME NOT NULL,
    closed_at DATETIME NULL,
    opening_cash_counted DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    expected_cash_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    closing_cash_counted DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    cash_difference DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    opened_by_user_id INT UNSIGNED NULL,
    closed_by_user_id INT UNSIGNED NULL,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_pos_session_company_number (company_id, session_number),
    INDEX idx_pos_session_company (company_id),
    INDEX idx_pos_session_register (register_id),
    INDEX idx_pos_session_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_orders (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    session_id BIGINT UNSIGNED NOT NULL,
    order_number VARCHAR(60) NOT NULL,
    receipt_number VARCHAR(80) NOT NULL DEFAULT '',
    order_date DATETIME NOT NULL,
    order_type VARCHAR(20) NOT NULL DEFAULT 'sale',
    original_order_number VARCHAR(60) NOT NULL DEFAULT '',
    customer_code VARCHAR(60) NOT NULL DEFAULT '',
    cashier_user_id INT UNSIGNED NULL,
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    change_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'paid',
    invoice_requested TINYINT(1) NOT NULL DEFAULT 0,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_pos_order_company_number (company_id, order_number),
    INDEX idx_pos_order_company (company_id),
    INDEX idx_pos_order_session (session_id),
    INDEX idx_pos_order_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_order_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    order_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    sku VARCHAR(80) NOT NULL DEFAULT '',
    product_name VARCHAR(190) NOT NULL DEFAULT '',
    quantity DECIMAL(14,3) NOT NULL DEFAULT 0.000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    discount_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    customer_note VARCHAR(255) NOT NULL DEFAULT '',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_pos_line_company (company_id),
    INDEX idx_pos_line_order (order_id),
    INDEX idx_pos_line_sku (sku)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_payments (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    order_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'cash',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    reference VARCHAR(120) NOT NULL DEFAULT '',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_pos_payment_company (company_id),
    INDEX idx_pos_payment_order (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS pos_cash_movements (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    session_id BIGINT UNSIGNED NOT NULL,
    movement_number VARCHAR(60) NOT NULL,
    movement_date DATETIME NOT NULL,
    movement_type VARCHAR(20) NOT NULL DEFAULT 'cash_in',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    reason VARCHAR(255) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'posted',
    created_by_user_id INT UNSIGNED NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_pos_cash_company_number (company_id, movement_number),
    INDEX idx_pos_cash_company (company_id),
    INDEX idx_pos_cash_session (session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS cost_centers (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    center_code VARCHAR(60) NOT NULL,
    center_name VARCHAR(180) NOT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_cost_center_company_code (company_id, center_code),
    INDEX idx_cost_center_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS projects (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    project_code VARCHAR(60) NOT NULL,
    project_name VARCHAR(190) NOT NULL,
    client_name VARCHAR(190) NOT NULL DEFAULT '',
    start_date DATE NULL,
    end_date DATE NULL,
    budget_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(30) NOT NULL DEFAULT 'planned',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_projects_company_code (company_id, project_code),
    INDEX idx_projects_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS budgets (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    fiscal_year VARCHAR(20) NOT NULL,
    budget_name VARCHAR(190) NOT NULL,
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    budget_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_budgets_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS journal_entries (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    entry_number VARCHAR(60) NOT NULL,
    entry_date DATE NOT NULL,
    reference VARCHAR(120) NOT NULL DEFAULT '',
    description TEXT NULL,
    total_debit DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    total_credit DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    source_type VARCHAR(40) NOT NULL DEFAULT '',
    source_id BIGINT UNSIGNED NULL,
    is_system_generated TINYINT(1) NOT NULL DEFAULT 0,
    created_by_user_id INT UNSIGNED NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_journal_company_number (company_id, entry_number),
    UNIQUE KEY uq_journal_source (company_id, source_type, source_id),
    INDEX idx_journal_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS journal_entry_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    journal_entry_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    account_code VARCHAR(60) NOT NULL,
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    debit DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    credit DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    description VARCHAR(255) NOT NULL DEFAULT '',
    source_type VARCHAR(40) NOT NULL DEFAULT '',
    source_id BIGINT UNSIGNED NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_journal_lines_company (company_id),
    INDEX idx_journal_lines_entry (journal_entry_id),
    INDEX idx_journal_lines_account (account_code),
    CONSTRAINT fk_journal_lines_entry FOREIGN KEY (journal_entry_id)
        REFERENCES journal_entries (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS sales_invoices (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    invoice_number VARCHAR(60) NOT NULL,
    invoice_date DATE NOT NULL,
    customer_code VARCHAR(60) NOT NULL DEFAULT '',
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    withholding_tax_name VARCHAR(120) NOT NULL DEFAULT '',
    withholding_tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    withholding_tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    net_payable DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_sales_company_number (company_id, invoice_number),
    INDEX idx_sales_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS purchase_bills (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    bill_number VARCHAR(60) NOT NULL,
    bill_date DATE NOT NULL,
    vendor_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_purchase_company_number (company_id, bill_number),
    INDEX idx_purchase_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS expenses (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    expense_number VARCHAR(60) NOT NULL,
    expense_date DATE NOT NULL,
    payee_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_expenses_company_number (company_id, expense_number),
    INDEX idx_expenses_company (company_id),
    INDEX idx_expenses_payee (payee_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS sales_returns (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    return_number VARCHAR(60) NOT NULL,
    return_date DATE NOT NULL,
    return_type VARCHAR(20) NOT NULL DEFAULT 'partial',
    original_invoice_id BIGINT UNSIGNED NULL,
    customer_code VARCHAR(60) NOT NULL DEFAULT '',
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_sales_returns_company_number (company_id, return_number),
    INDEX idx_sales_returns_company (company_id),
    INDEX idx_sales_returns_invoice (original_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS purchase_returns (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    return_number VARCHAR(60) NOT NULL,
    return_date DATE NOT NULL,
    return_type VARCHAR(20) NOT NULL DEFAULT 'partial',
    original_bill_id BIGINT UNSIGNED NULL,
    vendor_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_purchase_returns_company_number (company_id, return_number),
    INDEX idx_purchase_returns_company (company_id),
    INDEX idx_purchase_returns_bill (original_bill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS expense_returns (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    return_number VARCHAR(60) NOT NULL,
    return_date DATE NOT NULL,
    return_type VARCHAR(20) NOT NULL DEFAULT 'partial',
    original_expense_id BIGINT UNSIGNED NULL,
    payee_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 1,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 15.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_expense_returns_company_number (company_id, return_number),
    INDEX idx_expense_returns_company (company_id),
    INDEX idx_expense_returns_expense (original_expense_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS payments (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    payment_number VARCHAR(60) NOT NULL,
    payment_date DATE NOT NULL,
    direction VARCHAR(10) NOT NULL DEFAULT 'in',
    payment_method VARCHAR(20) NOT NULL DEFAULT 'cash',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    reference VARCHAR(120) NOT NULL DEFAULT '',
    sales_invoice_id BIGINT UNSIGNED NULL,
    purchase_bill_id BIGINT UNSIGNED NULL,
    expense_id BIGINT UNSIGNED NULL,
    ifrs16_lease_id BIGINT UNSIGNED NULL,
    ifrs16_schedule_id BIGINT UNSIGNED NULL,
    contact_code VARCHAR(60) NOT NULL DEFAULT '',
    bank_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_payment_company_number (company_id, payment_number),
    INDEX idx_payments_sales_invoice (sales_invoice_id),
    INDEX idx_payments_purchase_bill (purchase_bill_id),
    INDEX idx_payments_expense (expense_id),
    INDEX idx_payments_ifrs16_lease (ifrs16_lease_id),
    INDEX idx_payments_ifrs16_schedule (ifrs16_schedule_id),
    INDEX idx_payments_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS assets (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    asset_code VARCHAR(60) NOT NULL,
    asset_name VARCHAR(190) NOT NULL,
    purchase_date DATE NULL,
    purchase_cost DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    useful_life_months INT NOT NULL DEFAULT 0,
    salvage_value DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    depreciation_method VARCHAR(30) NOT NULL DEFAULT 'straight_line',
    accumulated_depreciation DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_assets_company_code (company_id, asset_code),
    INDEX idx_assets_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_departments (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    department_code VARCHAR(60) NOT NULL,
    department_name VARCHAR(190) NOT NULL,
    manager_name VARCHAR(190) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_departments_company_code (company_id, department_code),
    INDEX idx_hr_departments_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_job_titles (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    job_code VARCHAR(60) NOT NULL,
    job_title VARCHAR(190) NOT NULL,
    department_code VARCHAR(60) NOT NULL DEFAULT '',
    grade_name VARCHAR(80) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_jobs_company_code (company_id, job_code),
    INDEX idx_hr_jobs_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_shifts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    shift_code VARCHAR(60) NOT NULL,
    shift_name VARCHAR(190) NOT NULL,
    start_time TIME NULL,
    end_time TIME NULL,
    working_hours DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_shifts_company_code (company_id, shift_code),
    INDEX idx_hr_shifts_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_leave_types (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    leave_type_code VARCHAR(60) NOT NULL,
    leave_type_name VARCHAR(190) NOT NULL,
    annual_balance_days DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    is_paid TINYINT(1) NOT NULL DEFAULT 1,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_leave_types_company_code (company_id, leave_type_code),
    INDEX idx_hr_leave_types_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_employee_contracts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    contract_number VARCHAR(60) NOT NULL,
    employee_number VARCHAR(60) NOT NULL DEFAULT '',
    department_code VARCHAR(60) NOT NULL DEFAULT '',
    job_code VARCHAR(60) NOT NULL DEFAULT '',
    contract_type VARCHAR(40) NOT NULL DEFAULT 'full_time',
    start_date DATE NOT NULL,
    end_date DATE NULL,
    basic_salary DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    allowance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    insurance_number VARCHAR(80) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_contracts_company_number (company_id, contract_number),
    INDEX idx_hr_contracts_company (company_id),
    INDEX idx_hr_contracts_employee (employee_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_salary_components (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    component_code VARCHAR(60) NOT NULL,
    component_name VARCHAR(190) NOT NULL,
    component_type VARCHAR(30) NOT NULL DEFAULT 'allowance',
    calculation_type VARCHAR(30) NOT NULL DEFAULT 'fixed',
    default_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    percent_value DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_salary_components_company_code (company_id, component_code),
    INDEX idx_hr_salary_components_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS employees (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    employee_number VARCHAR(60) NOT NULL,
    full_name VARCHAR(190) NOT NULL,
    department_code VARCHAR(60) NOT NULL DEFAULT '',
    job_code VARCHAR(60) NOT NULL DEFAULT '',
    department VARCHAR(120) NOT NULL DEFAULT '',
    basic_salary DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    allowance DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    deduction_default DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_employees_company_number (company_id, employee_number),
    INDEX idx_employees_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_attendance (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    attendance_number VARCHAR(60) NOT NULL,
    employee_number VARCHAR(60) NOT NULL DEFAULT '',
    shift_code VARCHAR(60) NOT NULL DEFAULT '',
    attendance_date DATE NOT NULL,
    check_in TIME NULL,
    check_out TIME NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'present',
    hours_worked DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_attendance_company_number (company_id, attendance_number),
    INDEX idx_hr_attendance_company (company_id),
    INDEX idx_hr_attendance_employee (employee_number),
    INDEX idx_hr_attendance_date (attendance_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_leave_requests (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    request_number VARCHAR(60) NOT NULL,
    employee_number VARCHAR(60) NOT NULL DEFAULT '',
    leave_type_code VARCHAR(60) NOT NULL DEFAULT '',
    leave_type VARCHAR(40) NOT NULL DEFAULT 'annual',
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    days_count DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_leave_company_number (company_id, request_number),
    INDEX idx_hr_leave_company (company_id),
    INDEX idx_hr_leave_employee (employee_number),
    INDEX idx_hr_leave_dates (start_date, end_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS payroll_runs (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    run_number VARCHAR(60) NOT NULL,
    period_month INT NOT NULL DEFAULT 1,
    period_year INT NOT NULL DEFAULT 2026,
    total_gross DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    total_deductions DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    total_net DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_payroll_company_number (company_id, run_number),
    INDEX idx_payroll_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS hr_payroll_items (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    item_number VARCHAR(60) NOT NULL,
    run_number VARCHAR(60) NOT NULL DEFAULT '',
    employee_number VARCHAR(60) NOT NULL DEFAULT '',
    component_code VARCHAR(60) NOT NULL DEFAULT '',
    component_type VARCHAR(30) NOT NULL DEFAULT 'allowance',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_hr_payroll_items_company_number (company_id, item_number),
    INDEX idx_hr_payroll_items_company (company_id),
    INDEX idx_hr_payroll_items_run (run_number),
    INDEX idx_hr_payroll_items_employee (employee_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS inventory_movements (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    movement_number VARCHAR(60) NOT NULL,
    movement_date DATE NOT NULL,
    movement_type VARCHAR(20) NOT NULL DEFAULT 'in',
    sku VARCHAR(80) NOT NULL DEFAULT '',
    quantity DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    unit_cost DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    reference VARCHAR(120) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_inventory_company_number (company_id, movement_number),
    INDEX idx_inventory_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS report_templates (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    template_name VARCHAR(190) NOT NULL,
    module_scope VARCHAR(80) NOT NULL DEFAULT '',
    chart_type VARCHAR(20) NOT NULL DEFAULT 'bar',
    config_json LONGTEXT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_templates_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS financial_statement_groups (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    group_code VARCHAR(60) NOT NULL,
    group_name VARCHAR(190) NOT NULL,
    statement_type VARCHAR(40) NOT NULL DEFAULT 'balance_sheet',
    main_section VARCHAR(80) NOT NULL DEFAULT '',
    sort_order INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_fsg_company_code (company_id, group_code),
    INDEX idx_fsg_company (company_id),
    INDEX idx_fsg_statement (statement_type),
    INDEX idx_fsg_section (main_section)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ecl_countries (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    country_code VARCHAR(40) NOT NULL,
    country_name VARCHAR(190) NOT NULL,
    sp_rating VARCHAR(20) NOT NULL DEFAULT '',
    moodys_rating VARCHAR(20) NOT NULL DEFAULT '',
    fitch_rating VARCHAR(20) NOT NULL DEFAULT '',
    base_scenario_factor DECIMAL(14,8) NOT NULL DEFAULT 0.00000000,
    best_scenario_factor DECIMAL(14,8) NOT NULL DEFAULT 0.00000000,
    worst_scenario_factor DECIMAL(14,8) NOT NULL DEFAULT 0.00000000,
    base_weight_percent DECIMAL(8,4) NOT NULL DEFAULT 60.0000,
    best_weight_percent DECIMAL(8,4) NOT NULL DEFAULT 10.0000,
    worst_weight_percent DECIMAL(8,4) NOT NULL DEFAULT 30.0000,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ecl_country_company_code (company_id, country_code),
    INDEX idx_ecl_country_company (company_id),
    INDEX idx_ecl_country_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ecl_default_rates (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    agency VARCHAR(20) NOT NULL,
    rating_code VARCHAR(20) NOT NULL,
    one_year_pd_percent DECIMAL(10,6) NOT NULL DEFAULT 0.000000,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ecl_rate_company_agency_rating (company_id, agency, rating_code),
    INDEX idx_ecl_rate_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ecl_receivable_buckets (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    bucket_code VARCHAR(40) NOT NULL,
    bucket_name VARCHAR(190) NOT NULL,
    min_days INT NOT NULL DEFAULT 0,
    max_days INT NOT NULL DEFAULT 0,
    loss_rate_percent DECIMAL(10,6) NOT NULL DEFAULT 0.000000,
    sort_order INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ecl_bucket_company_code (company_id, bucket_code),
    INDEX idx_ecl_bucket_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ecl_report_categories (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    category_code VARCHAR(60) NOT NULL,
    category_name VARCHAR(190) NOT NULL,
    source_type VARCHAR(40) NOT NULL DEFAULT 'account_balances',
    loss_model VARCHAR(40) NOT NULL DEFAULT 'country_12m',
    country_code VARCHAR(40) NOT NULL DEFAULT '',
    account_codes TEXT NULL,
    financial_statement_group_code VARCHAR(60) NOT NULL DEFAULT '',
    manual_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    lgd_percent DECIMAL(8,4) NOT NULL DEFAULT 45.0000,
    rate_override_percent DECIMAL(10,6) NOT NULL DEFAULT 0.000000,
    sort_order INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ecl_category_company_code (company_id, category_code),
    INDEX idx_ecl_category_company (company_id),
    INDEX idx_ecl_category_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS posting_account_settings (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    sales_receivable_account_code VARCHAR(60) NOT NULL DEFAULT '',
    purchase_payable_account_code VARCHAR(60) NOT NULL DEFAULT '',
    accrued_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    prepaid_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_rou_asset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_lease_liability_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_interest_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_depreciation_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_accumulated_depreciation_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_payment_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_restoration_provision_account_code VARCHAR(60) NOT NULL DEFAULT '',
    ifrs16_lease_incentive_account_code VARCHAR(60) NOT NULL DEFAULT '',
    pos_sales_revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    pos_cash_over_short_gain_account_code VARCHAR(60) NOT NULL DEFAULT '',
    pos_cash_over_short_loss_account_code VARCHAR(60) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_posting_settings_company (company_id),
    INDEX idx_posting_settings_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);
    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_stages (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    stage_code VARCHAR(60) NOT NULL,
    stage_name VARCHAR(190) NOT NULL,
    sort_order INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_stages_company_code (company_id, stage_code),
    INDEX idx_student_stages_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_grades (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    grade_code VARCHAR(60) NOT NULL,
    grade_name VARCHAR(190) NOT NULL,
    stage_code VARCHAR(60) NOT NULL DEFAULT '',
    sort_order INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_grades_company_code (company_id, grade_code),
    INDEX idx_student_grades_company (company_id),
    INDEX idx_student_grades_stage (stage_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_classes (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    class_code VARCHAR(60) NOT NULL,
    class_name VARCHAR(190) NOT NULL,
    academic_year VARCHAR(20) NOT NULL DEFAULT '',
    stage_code VARCHAR(60) NOT NULL DEFAULT '',
    grade_code VARCHAR(60) NOT NULL DEFAULT '',
    capacity INT NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_classes_company_code (company_id, class_code),
    INDEX idx_student_classes_company (company_id),
    INDEX idx_student_classes_stage_grade (stage_code, grade_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS students (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    student_number VARCHAR(60) NOT NULL,
    student_name VARCHAR(190) NOT NULL,
    national_id VARCHAR(80) NOT NULL DEFAULT '',
    guardian_contact_code VARCHAR(60) NOT NULL DEFAULT '',
    stage_code VARCHAR(60) NOT NULL DEFAULT '',
    grade_code VARCHAR(60) NOT NULL DEFAULT '',
    class_code VARCHAR(60) NOT NULL DEFAULT '',
    enrollment_date DATE NULL,
    status VARCHAR(30) NOT NULL DEFAULT 'active',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_students_company_number (company_id, student_number),
    INDEX idx_students_company (company_id),
    INDEX idx_students_guardian (guardian_contact_code),
    INDEX idx_students_stage_grade (stage_code, grade_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_revenue_types (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    revenue_type_code VARCHAR(60) NOT NULL,
    revenue_type_name VARCHAR(190) NOT NULL,
    revenue_category VARCHAR(60) NOT NULL DEFAULT 'tuition',
    default_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_revenue_company_code (company_id, revenue_type_code),
    INDEX idx_student_revenue_company (company_id),
    INDEX idx_student_revenue_category (revenue_category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_fee_plans (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    fee_plan_code VARCHAR(60) NOT NULL,
    fee_plan_name VARCHAR(190) NOT NULL,
    academic_year VARCHAR(20) NOT NULL DEFAULT '',
    stage_code VARCHAR(60) NOT NULL DEFAULT '',
    grade_code VARCHAR(60) NOT NULL DEFAULT '',
    revenue_type_code VARCHAR(60) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    installments_count INT NOT NULL DEFAULT 1,
    due_day INT NOT NULL DEFAULT 1,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_fee_plan_company_code (company_id, fee_plan_code),
    INDEX idx_student_fee_plan_company (company_id),
    INDEX idx_student_fee_plan_grade (stage_code, grade_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_account_settings (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    receivable_account_code VARCHAR(60) NOT NULL DEFAULT '',
    advance_account_code VARCHAR(60) NOT NULL DEFAULT '',
    discount_account_code VARCHAR(60) NOT NULL DEFAULT '',
    default_revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    default_cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_account_settings_company (company_id),
    INDEX idx_student_account_settings_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_invoices (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    student_invoice_number VARCHAR(60) NOT NULL,
    invoice_date DATE NOT NULL,
    academic_year VARCHAR(20) NOT NULL DEFAULT '',
    student_number VARCHAR(60) NOT NULL DEFAULT '',
    guardian_contact_code VARCHAR(60) NOT NULL DEFAULT '',
    stage_code VARCHAR(60) NOT NULL DEFAULT '',
    grade_code VARCHAR(60) NOT NULL DEFAULT '',
    class_code VARCHAR(60) NOT NULL DEFAULT '',
    revenue_type_code VARCHAR(60) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_invoices_company_number (company_id, student_invoice_number),
    INDEX idx_student_invoices_company (company_id),
    INDEX idx_student_invoices_student (student_number),
    INDEX idx_student_invoices_guardian (guardian_contact_code),
    INDEX idx_student_invoices_date (invoice_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS sales_invoice_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    sales_invoice_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_sales_invoice_lines_company (company_id),
    INDEX idx_sales_invoice_lines_parent (sales_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS purchase_bill_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    purchase_bill_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_purchase_bill_lines_company (company_id),
    INDEX idx_purchase_bill_lines_parent (purchase_bill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS expense_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    expense_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_expense_lines_company (company_id),
    INDEX idx_expense_lines_parent (expense_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS sales_return_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    sales_return_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_sales_return_lines_company (company_id),
    INDEX idx_sales_return_lines_parent (sales_return_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS purchase_return_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    purchase_return_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_purchase_return_lines_company (company_id),
    INDEX idx_purchase_return_lines_parent (purchase_return_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS expense_return_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    expense_return_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_expense_return_lines_company (company_id),
    INDEX idx_expense_return_lines_parent (expense_return_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_invoice_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    student_invoice_id BIGINT UNSIGNED NOT NULL,
    line_no INT UNSIGNED NOT NULL DEFAULT 1,
    item_code VARCHAR(80) NOT NULL DEFAULT '',
    description VARCHAR(255) NOT NULL DEFAULT '',
    account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    unit_price DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    discount_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    subtotal DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    line_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_student_invoice_lines_company (company_id),
    INDEX idx_student_invoice_lines_parent (student_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_receipts (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    receipt_number VARCHAR(60) NOT NULL,
    receipt_date DATE NOT NULL,
    student_invoice_id BIGINT UNSIGNED NULL,
    student_number VARCHAR(60) NOT NULL DEFAULT '',
    guardian_contact_code VARCHAR(60) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'cash',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    reference VARCHAR(120) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_receipts_company_number (company_id, receipt_number),
    INDEX idx_student_receipts_company (company_id),
    INDEX idx_student_receipts_invoice (student_invoice_id),
    INDEX idx_student_receipts_student (student_number),
    INDEX idx_student_receipts_date (receipt_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS student_refunds (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    voucher_number VARCHAR(60) NOT NULL,
    voucher_date DATE NOT NULL,
    student_number VARCHAR(60) NOT NULL DEFAULT '',
    guardian_contact_code VARCHAR(60) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'cash',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    reason VARCHAR(255) NOT NULL DEFAULT '',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_student_refunds_company_number (company_id, voucher_number),
    INDEX idx_student_refunds_company (company_id),
    INDEX idx_student_refunds_student (student_number),
    INDEX idx_student_refunds_date (voucher_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_hospitals (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    hospital_code VARCHAR(60) NOT NULL,
    hospital_name VARCHAR(190) NOT NULL,
    tax_number VARCHAR(80) NOT NULL DEFAULT '',
    phone VARCHAR(80) NOT NULL DEFAULT '',
    email VARCHAR(180) NOT NULL DEFAULT '',
    address TEXT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_hospitals_company_code (company_id, hospital_code),
    INDEX idx_excellence_hospitals_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_devices (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    device_code VARCHAR(60) NOT NULL,
    device_name VARCHAR(190) NOT NULL,
    serial_number VARCHAR(120) NOT NULL DEFAULT '',
    category VARCHAR(120) NOT NULL DEFAULT '',
    default_rental_rate DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    hospital_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_devices_company_code (company_id, device_code),
    INDEX idx_excellence_devices_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_operations (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    operation_number VARCHAR(60) NOT NULL,
    operation_date DATE NOT NULL,
    hospital_code VARCHAR(60) NOT NULL DEFAULT '',
    patient_name VARCHAR(190) NOT NULL DEFAULT '',
    patient_file_no VARCHAR(120) NOT NULL DEFAULT '',
    procedure_name VARCHAR(190) NOT NULL DEFAULT '',
    doctor_name VARCHAR(190) NOT NULL DEFAULT '',
    device_code VARCHAR(60) NOT NULL DEFAULT '',
    quantity DECIMAL(14,4) NOT NULL DEFAULT 1.0000,
    rental_rate DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    revenue_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    hospital_expense_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    net_operation_income DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    revenue_account_code VARCHAR(60) NOT NULL DEFAULT '',
    hospital_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_operations_company_number (company_id, operation_number),
    INDEX idx_excellence_operations_company (company_id),
    INDEX idx_excellence_operations_hospital (hospital_code),
    INDEX idx_excellence_operations_date (operation_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_hospital_expenses (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    expense_number VARCHAR(60) NOT NULL,
    expense_date DATE NOT NULL,
    hospital_code VARCHAR(60) NOT NULL DEFAULT '',
    operation_number VARCHAR(60) NOT NULL DEFAULT '',
    settlement_mode VARCHAR(30) NOT NULL DEFAULT 'accrued_operation',
    expense_type VARCHAR(60) NOT NULL DEFAULT 'hospital_share',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'bank',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    offset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_hospital_expenses_company_number (company_id, expense_number),
    INDEX idx_excellence_hospital_expenses_company (company_id),
    INDEX idx_excellence_hospital_expenses_date (expense_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_admin_expenses (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    expense_number VARCHAR(60) NOT NULL,
    expense_date DATE NOT NULL,
    category VARCHAR(120) NOT NULL DEFAULT 'administration',
    description VARCHAR(255) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'bank',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_admin_expenses_company_number (company_id, expense_number),
    INDEX idx_excellence_admin_expenses_company (company_id),
    INDEX idx_excellence_admin_expenses_date (expense_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS excellence_salaries (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    salary_number VARCHAR(60) NOT NULL,
    salary_date DATE NOT NULL,
    employee_name VARCHAR(190) NOT NULL DEFAULT '',
    role_title VARCHAR(160) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT 'bank',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    salary_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_excellence_salaries_company_number (company_id, salary_number),
    INDEX idx_excellence_salaries_company (company_id),
    INDEX idx_excellence_salaries_date (salary_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);
    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS prepaid_expenses (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    prepaid_number VARCHAR(60) NOT NULL,
    prepaid_date DATE NOT NULL,
    description VARCHAR(255) NOT NULL DEFAULT '',
    contact_code VARCHAR(60) NOT NULL DEFAULT '',
    prepaid_account_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    total_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    is_taxable TINYINT(1) NOT NULL DEFAULT 0,
    tax_rate_percent DECIMAL(8,2) NOT NULL DEFAULT 0.00,
    tax_name VARCHAR(120) NOT NULL DEFAULT '',
    tax_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    grand_total DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    inline_paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_method VARCHAR(20) NOT NULL DEFAULT '',
    payment_account_name VARCHAR(160) NOT NULL DEFAULT '',
    start_date DATE NOT NULL,
    duration_months INT NOT NULL DEFAULT 1,
    monthly_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    amortized_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_prepaid_expenses_company_number (company_id, prepaid_number),
    INDEX idx_prepaid_expenses_company (company_id),
    INDEX idx_prepaid_expenses_date (prepaid_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS prepaid_amortization_runs (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    run_number VARCHAR(60) NOT NULL,
    run_date DATE NOT NULL,
    period_month INT NOT NULL DEFAULT 1,
    period_year INT NOT NULL DEFAULT 2026,
    prepaid_expense_id BIGINT UNSIGNED NOT NULL,
    prepaid_number VARCHAR(60) NOT NULL DEFAULT '',
    month_no INT NOT NULL DEFAULT 1,
    prepaid_account_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    journal_entry_id BIGINT UNSIGNED NULL,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_prepaid_amort_company_number (company_id, run_number),
    UNIQUE KEY uq_prepaid_amort_company_period (company_id, prepaid_expense_id, period_year, period_month),
    INDEX idx_prepaid_amort_company (company_id),
    INDEX idx_prepaid_amort_date (run_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS depreciation_runs (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    run_number VARCHAR(60) NOT NULL,
    run_date DATE NOT NULL,
    period_month INT NOT NULL DEFAULT 1,
    period_year INT NOT NULL DEFAULT 2026,
    asset_id BIGINT UNSIGNED NULL,
    asset_code VARCHAR(60) NOT NULL DEFAULT '',
    schedule_start_date DATE NULL,
    duration_months INT NOT NULL DEFAULT 1,
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    accumulated_account_code VARCHAR(60) NOT NULL DEFAULT '',
    total_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    assets_count INT NOT NULL DEFAULT 0,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    journal_entry_id BIGINT UNSIGNED NULL,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_depreciation_runs_company_number (company_id, run_number),
    UNIQUE KEY uq_depreciation_runs_company_asset_period (company_id, asset_id, period_year, period_month),
    INDEX idx_depreciation_runs_company (company_id),
    INDEX idx_depreciation_runs_date (run_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS depreciation_run_lines (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    run_id BIGINT UNSIGNED NOT NULL,
    asset_id BIGINT UNSIGNED NOT NULL,
    asset_code VARCHAR(60) NOT NULL,
    asset_name VARCHAR(190) NOT NULL,
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    depreciation_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    previous_accumulated DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    new_accumulated DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_depreciation_lines_company (company_id),
    INDEX idx_depreciation_lines_run (run_id),
    INDEX idx_depreciation_lines_asset (asset_id),
    CONSTRAINT fk_depreciation_lines_run FOREIGN KEY (run_id)
        REFERENCES depreciation_runs (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ifrs16_leases (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    lease_number VARCHAR(60) NOT NULL,
    lease_name VARCHAR(190) NOT NULL,
    lessor_code VARCHAR(60) NOT NULL DEFAULT '',
    country VARCHAR(120) NOT NULL DEFAULT 'Saudi Arabia',
    currency_code VARCHAR(10) NOT NULL DEFAULT 'SAR',
    commencement_date DATE NOT NULL,
    end_date DATE NOT NULL,
    first_payment_date DATE NOT NULL,
    payment_frequency VARCHAR(20) NOT NULL DEFAULT 'monthly',
    payment_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    number_of_payments INT NOT NULL DEFAULT 0,
    payments_at_or_before_commencement DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    initial_direct_costs DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    restoration_cost DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    lease_incentives DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    benchmark_rate_percent DECIMAL(9,4) NOT NULL DEFAULT 4.2500,
    credit_spread_percent DECIMAL(9,4) NOT NULL DEFAULT 2.0000,
    manual_discount_rate_percent DECIMAL(9,4) NOT NULL DEFAULT 0.0000,
    discount_rate_percent DECIMAL(9,4) NOT NULL DEFAULT 6.2500,
    lease_term_days INT NOT NULL DEFAULT 0,
    lease_term_months INT NOT NULL DEFAULT 0,
    pv_lease_liability DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    initial_rou_asset DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    daily_depreciation DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    monthly_depreciation DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    total_interest DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    closing_liability_check DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    rou_asset_account_code VARCHAR(60) NOT NULL DEFAULT '',
    lease_liability_account_code VARCHAR(60) NOT NULL DEFAULT '',
    interest_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    depreciation_expense_account_code VARCHAR(60) NOT NULL DEFAULT '',
    accumulated_depreciation_account_code VARCHAR(60) NOT NULL DEFAULT '',
    payment_account_code VARCHAR(60) NOT NULL DEFAULT '',
    restoration_provision_account_code VARCHAR(60) NOT NULL DEFAULT '',
    lease_incentive_account_code VARCHAR(60) NOT NULL DEFAULT '',
    cost_center_code VARCHAR(60) NOT NULL DEFAULT '',
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    journal_entry_id BIGINT UNSIGNED NULL,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ifrs16_leases_company_number (company_id, lease_number),
    INDEX idx_ifrs16_leases_company (company_id),
    INDEX idx_ifrs16_leases_dates (commencement_date, end_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ifrs16_lease_schedules (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    lease_id BIGINT UNSIGNED NOT NULL,
    lease_number VARCHAR(60) NOT NULL DEFAULT '',
    sequence_no INT NOT NULL DEFAULT 1,
    schedule_date DATE NOT NULL,
    opening_liability DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    days_count INT NOT NULL DEFAULT 0,
    interest_expense DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    liability_reduction DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    closing_liability DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    depreciation_expense DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    accumulated_depreciation DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    rou_asset_nbv DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    journal_entry_id BIGINT UNSIGNED NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ifrs16_schedule_lease_seq (company_id, lease_id, sequence_no),
    INDEX idx_ifrs16_schedule_company (company_id),
    INDEX idx_ifrs16_schedule_lease (lease_id),
    INDEX idx_ifrs16_schedule_date (schedule_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS ifrs16_lease_payments (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    lease_id BIGINT UNSIGNED NOT NULL,
    sequence_no INT NOT NULL DEFAULT 1,
    payment_date DATE NOT NULL,
    payment_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    payment_type VARCHAR(80) NOT NULL DEFAULT 'Fixed',
    included_in_liability TINYINT(1) NOT NULL DEFAULT 1,
    days_from_commencement INT NOT NULL DEFAULT 0,
    discount_factor DECIMAL(18,10) NOT NULL DEFAULT 0.0000000000,
    pv_at_commencement DECIMAL(14,2) NOT NULL DEFAULT 0.00,
    notes TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_ifrs16_payment_lease_seq (company_id, lease_id, sequence_no),
    INDEX idx_ifrs16_payment_company (company_id),
    INDEX idx_ifrs16_payment_lease (lease_id),
    INDEX idx_ifrs16_payment_date (payment_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL);

    app_add_column_if_missing($pdo, 'bank_accounts', 'gl_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER bank_name");
    app_add_column_if_missing($pdo, 'cash_accounts', 'gl_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER cash_name");
    app_add_column_if_missing($pdo, 'tax_rates', 'tax_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER applies_to");
    app_add_column_if_missing($pdo, 'contacts', 'withholding_tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER tax_number");
    app_add_column_if_missing($pdo, 'contacts', 'withholding_tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 0.00 AFTER withholding_tax_name');
    app_add_column_if_missing($pdo, 'sales_invoices', 'revenue_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER customer_code");
    app_add_column_if_missing($pdo, 'sales_invoices', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER revenue_account_code");
    app_add_column_if_missing($pdo, 'sales_invoices', 'is_taxable', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER revenue_account_code');
    app_add_column_if_missing($pdo, 'sales_invoices', 'tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 15.00 AFTER is_taxable');
    app_add_column_if_missing($pdo, 'sales_invoices', 'tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER tax_rate_percent");
    app_add_column_if_missing($pdo, 'sales_invoices', 'inline_paid_amount', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER grand_total');
    app_add_column_if_missing($pdo, 'sales_invoices', 'payment_method', "VARCHAR(20) NOT NULL DEFAULT '' AFTER paid_amount");
    app_add_column_if_missing($pdo, 'sales_invoices', 'payment_account_name', "VARCHAR(160) NOT NULL DEFAULT '' AFTER payment_method");
    app_add_column_if_missing($pdo, 'purchase_bills', 'expense_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER vendor_code");
    app_add_column_if_missing($pdo, 'purchase_bills', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER expense_account_code");
    app_add_column_if_missing($pdo, 'purchase_bills', 'is_taxable', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER expense_account_code');
    app_add_column_if_missing($pdo, 'purchase_bills', 'tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 15.00 AFTER is_taxable');
    app_add_column_if_missing($pdo, 'purchase_bills', 'tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER tax_rate_percent");
    app_add_column_if_missing($pdo, 'purchase_bills', 'withholding_tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER grand_total");
    app_add_column_if_missing($pdo, 'purchase_bills', 'withholding_tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 0.00 AFTER withholding_tax_name');
    app_add_column_if_missing($pdo, 'purchase_bills', 'withholding_tax_total', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER withholding_tax_rate_percent');
    app_add_column_if_missing($pdo, 'purchase_bills', 'net_payable', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER withholding_tax_total');
    app_add_column_if_missing($pdo, 'purchase_bills', 'inline_paid_amount', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER grand_total');
    app_add_column_if_missing($pdo, 'purchase_bills', 'payment_method', "VARCHAR(20) NOT NULL DEFAULT '' AFTER paid_amount");
    app_add_column_if_missing($pdo, 'purchase_bills', 'payment_account_name', "VARCHAR(160) NOT NULL DEFAULT '' AFTER payment_method");
    app_add_column_if_missing($pdo, 'expenses', 'expense_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER payee_code");
    app_add_column_if_missing($pdo, 'expenses', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER expense_account_code");
    app_add_column_if_missing($pdo, 'expenses', 'is_taxable', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER expense_account_code');
    app_add_column_if_missing($pdo, 'expenses', 'tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 15.00 AFTER is_taxable');
    app_add_column_if_missing($pdo, 'expenses', 'tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER tax_rate_percent");
    app_add_column_if_missing($pdo, 'expenses', 'inline_paid_amount', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER grand_total');
    app_add_column_if_missing($pdo, 'expenses', 'payment_method', "VARCHAR(20) NOT NULL DEFAULT '' AFTER paid_amount");
    app_add_column_if_missing($pdo, 'expenses', 'payment_account_name', "VARCHAR(160) NOT NULL DEFAULT '' AFTER payment_method");
    app_add_column_if_missing($pdo, 'expenses', 'offset_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER payment_account_name");
    app_add_column_if_missing($pdo, 'prepaid_expenses', 'is_taxable', 'TINYINT(1) NOT NULL DEFAULT 0 AFTER total_amount');
    app_add_column_if_missing($pdo, 'prepaid_expenses', 'tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 0.00 AFTER is_taxable');
    app_add_column_if_missing($pdo, 'prepaid_expenses', 'tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER tax_rate_percent");
    app_add_column_if_missing($pdo, 'prepaid_expenses', 'tax_total', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER tax_name');
    app_add_column_if_missing($pdo, 'prepaid_expenses', 'grand_total', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER tax_total');
    app_add_column_if_missing($pdo, 'chart_of_accounts', 'financial_statement_group_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER opening_balance");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_rou_asset_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER prepaid_expense_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_lease_liability_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_rou_asset_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_interest_expense_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_lease_liability_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_depreciation_expense_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_interest_expense_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_accumulated_depreciation_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_depreciation_expense_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_payment_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_accumulated_depreciation_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_restoration_provision_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_payment_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'ifrs16_lease_incentive_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_restoration_provision_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'pos_sales_revenue_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER ifrs16_lease_incentive_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'pos_cash_over_short_gain_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER pos_sales_revenue_account_code");
    app_add_column_if_missing($pdo, 'posting_account_settings', 'pos_cash_over_short_loss_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER pos_cash_over_short_gain_account_code");
    app_add_column_if_missing($pdo, 'payments', 'sales_invoice_id', 'BIGINT UNSIGNED NULL AFTER reference');
    app_add_column_if_missing($pdo, 'payments', 'purchase_bill_id', 'BIGINT UNSIGNED NULL AFTER reference');
    app_add_column_if_missing($pdo, 'payments', 'expense_id', 'BIGINT UNSIGNED NULL AFTER purchase_bill_id');
    app_add_column_if_missing($pdo, 'payments', 'ifrs16_lease_id', 'BIGINT UNSIGNED NULL AFTER expense_id');
    app_add_column_if_missing($pdo, 'payments', 'ifrs16_schedule_id', 'BIGINT UNSIGNED NULL AFTER ifrs16_lease_id');
    app_add_column_if_missing($pdo, 'payments', 'offset_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER bank_account_name");
    app_add_column_if_missing($pdo, 'journal_entries', 'source_type', "VARCHAR(40) NOT NULL DEFAULT '' AFTER status");
    app_add_column_if_missing($pdo, 'journal_entries', 'source_id', 'BIGINT UNSIGNED NULL AFTER source_type');
    app_add_column_if_missing($pdo, 'journal_entries', 'is_system_generated', 'TINYINT(1) NOT NULL DEFAULT 0 AFTER source_id');
    app_add_column_if_missing($pdo, 'journal_entry_lines', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER account_code");
    app_add_column_if_missing($pdo, 'companies', 'vat_number', "VARCHAR(80) NOT NULL DEFAULT '' AFTER legal_name");
    $pdo->exec("UPDATE companies SET vat_number = '300000000000003' WHERE vat_number = '' OR vat_number IS NULL");
    app_add_column_if_missing($pdo, 'students', 'student_name', "VARCHAR(190) NOT NULL DEFAULT '' AFTER student_number");
    app_add_column_if_missing($pdo, 'students', 'guardian_contact_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER national_id");
    app_add_column_if_missing($pdo, 'students', 'stage_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER guardian_contact_code");
    app_add_column_if_missing($pdo, 'students', 'grade_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER stage_code");
    app_add_column_if_missing($pdo, 'students', 'class_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER grade_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'student_invoice_number', "VARCHAR(60) NOT NULL DEFAULT '' AFTER company_id");
    app_add_column_if_missing($pdo, 'student_invoices', 'guardian_contact_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER student_number");
    app_add_column_if_missing($pdo, 'student_invoices', 'stage_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER guardian_contact_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'grade_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER stage_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'class_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER grade_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'revenue_type_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER class_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'revenue_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER description");
    app_add_column_if_missing($pdo, 'student_invoices', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER revenue_account_code");
    app_add_column_if_missing($pdo, 'student_invoices', 'is_taxable', 'TINYINT(1) NOT NULL DEFAULT 0 AFTER cost_center_code');
    app_add_column_if_missing($pdo, 'student_invoices', 'tax_name', "VARCHAR(120) NOT NULL DEFAULT '' AFTER is_taxable");
    app_add_column_if_missing($pdo, 'student_invoices', 'tax_rate_percent', 'DECIMAL(8,2) NOT NULL DEFAULT 0.00 AFTER tax_name');
    app_add_column_if_missing($pdo, 'student_invoices', 'subtotal', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER tax_rate_percent');
    app_add_column_if_missing($pdo, 'student_invoices', 'discount_total', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER subtotal');
    app_add_column_if_missing($pdo, 'student_invoices', 'tax_total', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER discount_total');
    app_add_column_if_missing($pdo, 'student_invoices', 'inline_paid_amount', 'DECIMAL(14,2) NOT NULL DEFAULT 0.00 AFTER grand_total');
    app_add_column_if_missing($pdo, 'student_invoices', 'payment_account_name', "VARCHAR(160) NOT NULL DEFAULT '' AFTER payment_method");
    if (app_column_exists($pdo, 'students', 'full_name')) {
        $pdo->exec("ALTER TABLE `students` MODIFY `full_name` VARCHAR(190) NOT NULL DEFAULT ''");
        $pdo->exec("UPDATE `students` SET `student_name` = `full_name` WHERE (`student_name` = '' OR `student_name` IS NULL) AND `full_name` <> ''");
    }
    foreach ([
        'grade_level' => "VARCHAR(60) NOT NULL DEFAULT ''",
        'class_section' => "VARCHAR(60) NOT NULL DEFAULT ''",
        'academic_year' => "VARCHAR(20) NOT NULL DEFAULT ''",
        'guardian_name' => "VARCHAR(190) NOT NULL DEFAULT ''",
        'guardian_phone' => "VARCHAR(40) NOT NULL DEFAULT ''",
        'guardian_email' => "VARCHAR(160) NOT NULL DEFAULT ''",
        'address' => "VARCHAR(500) NOT NULL DEFAULT ''",
        'notes' => "VARCHAR(500) NOT NULL DEFAULT ''",
    ] as $legacyColumn => $legacyDefinition) {
        if (app_column_exists($pdo, 'students', (string) $legacyColumn)) {
            $pdo->exec('ALTER TABLE `students` MODIFY `' . $legacyColumn . '` ' . $legacyDefinition);
        }
    }
    if (app_column_exists($pdo, 'student_invoices', 'invoice_number')) {
        $pdo->exec("ALTER TABLE `student_invoices` MODIFY `invoice_number` VARCHAR(60) NOT NULL DEFAULT ''");
        $pdo->exec("UPDATE `student_invoices` SET `student_invoice_number` = `invoice_number` WHERE (`student_invoice_number` = '' OR `student_invoice_number` IS NULL) AND `invoice_number` <> ''");
        if (app_index_exists($pdo, 'student_invoices', 'uq_student_inv_company_number')) {
            $pdo->exec('ALTER TABLE `student_invoices` DROP INDEX `uq_student_inv_company_number`');
        }
    }
    foreach ([
        'fee_code' => "VARCHAR(40) NOT NULL DEFAULT ''",
        'description' => "VARCHAR(255) NOT NULL DEFAULT ''",
        'payment_method' => "VARCHAR(20) NOT NULL DEFAULT ''",
        'academic_year' => "VARCHAR(20) NOT NULL DEFAULT ''",
        'semester' => "VARCHAR(20) NOT NULL DEFAULT ''",
        'notes' => "VARCHAR(500) NOT NULL DEFAULT ''",
    ] as $legacyColumn => $legacyDefinition) {
        if (app_column_exists($pdo, 'student_invoices', (string) $legacyColumn)) {
            $pdo->exec('ALTER TABLE `student_invoices` MODIFY `' . $legacyColumn . '` ' . $legacyDefinition);
        }
    }
    if (app_column_exists($pdo, 'student_invoices', 'amount')) {
        $pdo->exec('UPDATE `student_invoices` SET `subtotal` = `amount` WHERE (`subtotal` = 0 OR `subtotal` IS NULL) AND `amount` <> 0');
    }
    if (app_column_exists($pdo, 'student_invoices', 'discount_amount')) {
        $pdo->exec('UPDATE `student_invoices` SET `discount_total` = `discount_amount` WHERE (`discount_total` = 0 OR `discount_total` IS NULL) AND `discount_amount` <> 0');
    }
    $pdo->exec(
        "UPDATE student_invoices i
         LEFT JOIN student_account_settings s ON s.company_id = i.company_id
         SET i.cost_center_code = COALESCE(NULLIF(s.default_cost_center_code, ''), 'CC-ADM')
         WHERE i.cost_center_code = '' OR i.cost_center_code IS NULL"
    );
    $pdo->exec(
        "UPDATE student_invoices i
         LEFT JOIN student_account_settings s ON s.company_id = i.company_id
         SET i.revenue_account_code = COALESCE(NULLIF(s.default_revenue_account_code, ''), '4100')
         WHERE i.revenue_account_code = '' OR i.revenue_account_code IS NULL"
    );
    if (!app_index_exists($pdo, 'student_invoices', 'uq_student_invoices_company_number')) {
        try {
            $pdo->exec('ALTER TABLE `student_invoices` ADD UNIQUE KEY `uq_student_invoices_company_number` (`company_id`, `student_invoice_number`)');
        } catch (Throwable $e) {
            // Existing legacy duplicates should not block the rest of the upgrade.
        }
    }
    app_add_column_if_missing($pdo, 'assets', 'cost_center_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER purchase_cost");
    app_add_column_if_missing($pdo, 'assets', 'auto_depreciation_enabled', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER depreciation_method');
    app_add_column_if_missing($pdo, 'assets', 'depreciation_start_date', "DATE NULL AFTER auto_depreciation_enabled");
    app_add_column_if_missing($pdo, 'assets', 'depreciation_months', 'INT NOT NULL DEFAULT 0 AFTER depreciation_start_date');
    app_add_column_if_missing($pdo, 'assets', 'depreciation_expense_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER depreciation_months");
    app_add_column_if_missing($pdo, 'assets', 'depreciation_accumulated_account_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER depreciation_expense_account_code");
    app_add_column_if_missing($pdo, 'employees', 'department_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER full_name");
    app_add_column_if_missing($pdo, 'employees', 'job_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER department_code");
    app_add_column_if_missing($pdo, 'hr_attendance', 'shift_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER employee_number");
    app_add_column_if_missing($pdo, 'hr_leave_requests', 'leave_type_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER employee_number");
    app_add_column_if_missing($pdo, 'depreciation_runs', 'asset_id', 'BIGINT UNSIGNED NULL AFTER period_year');
    app_add_column_if_missing($pdo, 'depreciation_runs', 'asset_code', "VARCHAR(60) NOT NULL DEFAULT '' AFTER asset_id");
    app_add_column_if_missing($pdo, 'depreciation_runs', 'schedule_start_date', "DATE NULL AFTER asset_code");
    app_add_column_if_missing($pdo, 'depreciation_runs', 'duration_months', 'INT NOT NULL DEFAULT 1 AFTER schedule_start_date');
    $pdo->exec("ALTER TABLE `depreciation_runs` MODIFY `status` VARCHAR(20) NOT NULL DEFAULT 'draft'");
    if (app_index_exists($pdo, 'depreciation_runs', 'uq_depreciation_runs_company_period')) {
        $pdo->exec('ALTER TABLE `depreciation_runs` DROP INDEX `uq_depreciation_runs_company_period`');
    }
    if (!app_index_exists($pdo, 'depreciation_runs', 'uq_depreciation_runs_company_asset_period')) {
        $pdo->exec('ALTER TABLE `depreciation_runs` ADD UNIQUE KEY `uq_depreciation_runs_company_asset_period` (`company_id`, `asset_id`, `period_year`, `period_month`)');
    }

    if (!app_index_exists($pdo, 'payments', 'idx_payments_sales_invoice')) {
        $pdo->exec('ALTER TABLE `payments` ADD INDEX `idx_payments_sales_invoice` (`sales_invoice_id`)');
    }
    if (!app_index_exists($pdo, 'payments', 'idx_payments_purchase_bill')) {
        $pdo->exec('ALTER TABLE `payments` ADD INDEX `idx_payments_purchase_bill` (`purchase_bill_id`)');
    }
    if (!app_index_exists($pdo, 'payments', 'idx_payments_expense')) {
        $pdo->exec('ALTER TABLE `payments` ADD INDEX `idx_payments_expense` (`expense_id`)');
    }
    if (!app_index_exists($pdo, 'payments', 'idx_payments_ifrs16_lease')) {
        $pdo->exec('ALTER TABLE `payments` ADD INDEX `idx_payments_ifrs16_lease` (`ifrs16_lease_id`)');
    }
    if (!app_index_exists($pdo, 'payments', 'idx_payments_ifrs16_schedule')) {
        $pdo->exec('ALTER TABLE `payments` ADD INDEX `idx_payments_ifrs16_schedule` (`ifrs16_schedule_id`)');
    }
    if (!app_index_exists($pdo, 'chart_of_accounts', 'idx_coa_statement_group')) {
        $pdo->exec('ALTER TABLE `chart_of_accounts` ADD INDEX `idx_coa_statement_group` (`financial_statement_group_code`)');
    }

    if (!app_index_exists($pdo, 'journal_entries', 'uq_journal_source')) {
        $pdo->exec('ALTER TABLE `journal_entries` ADD UNIQUE KEY `uq_journal_source` (`company_id`, `source_type`, `source_id`)');
    }

    $pdo->exec(
        "INSERT IGNORE INTO student_account_settings
         (company_id, receivable_account_code, advance_account_code, discount_account_code, default_revenue_account_code, default_cost_center_code)
         SELECT id, '1121', '2140', '2140', '4100', 'CC-ADM'
         FROM companies"
    );
    $pdo->exec(
        "INSERT IGNORE INTO posting_account_settings
         (company_id, sales_receivable_account_code, purchase_payable_account_code, accrued_expense_account_code, prepaid_expense_account_code,
         ifrs16_rou_asset_account_code, ifrs16_lease_liability_account_code, ifrs16_interest_expense_account_code,
          ifrs16_depreciation_expense_account_code, ifrs16_accumulated_depreciation_account_code, ifrs16_payment_account_code,
          ifrs16_restoration_provision_account_code, ifrs16_lease_incentive_account_code,
          pos_sales_revenue_account_code, pos_cash_over_short_gain_account_code, pos_cash_over_short_loss_account_code)
         SELECT id, '1121', '2111', '2130', '1150', '1231', '2210', '5700', '5600', '1239', '1112', '2220', '1160', '4100', '4100', '5510'
         FROM companies"
    );
    $pdo->exec(
        "UPDATE posting_account_settings
         SET ifrs16_rou_asset_account_code = IF(ifrs16_rou_asset_account_code = '' OR ifrs16_rou_asset_account_code IS NULL, '1231', ifrs16_rou_asset_account_code),
             ifrs16_lease_liability_account_code = IF(ifrs16_lease_liability_account_code = '' OR ifrs16_lease_liability_account_code IS NULL, '2210', ifrs16_lease_liability_account_code),
             ifrs16_interest_expense_account_code = IF(ifrs16_interest_expense_account_code = '' OR ifrs16_interest_expense_account_code IS NULL, '5700', ifrs16_interest_expense_account_code),
             ifrs16_depreciation_expense_account_code = IF(ifrs16_depreciation_expense_account_code = '' OR ifrs16_depreciation_expense_account_code IS NULL, '5600', ifrs16_depreciation_expense_account_code),
             ifrs16_accumulated_depreciation_account_code = IF(ifrs16_accumulated_depreciation_account_code = '' OR ifrs16_accumulated_depreciation_account_code IS NULL, '1239', ifrs16_accumulated_depreciation_account_code),
             ifrs16_payment_account_code = IF(ifrs16_payment_account_code = '' OR ifrs16_payment_account_code IS NULL, '1112', ifrs16_payment_account_code),
             ifrs16_restoration_provision_account_code = IF(ifrs16_restoration_provision_account_code = '' OR ifrs16_restoration_provision_account_code IS NULL, '2220', ifrs16_restoration_provision_account_code),
             ifrs16_lease_incentive_account_code = IF(ifrs16_lease_incentive_account_code = '' OR ifrs16_lease_incentive_account_code IS NULL, '1160', ifrs16_lease_incentive_account_code),
             pos_sales_revenue_account_code = IF(pos_sales_revenue_account_code = '' OR pos_sales_revenue_account_code IS NULL, '4100', pos_sales_revenue_account_code),
             pos_cash_over_short_gain_account_code = IF(pos_cash_over_short_gain_account_code = '' OR pos_cash_over_short_gain_account_code IS NULL, '4100', pos_cash_over_short_gain_account_code),
             pos_cash_over_short_loss_account_code = IF(pos_cash_over_short_loss_account_code = '' OR pos_cash_over_short_loss_account_code IS NULL, '5510', pos_cash_over_short_loss_account_code)"
    );

    $pdo->exec(
        "UPDATE bank_accounts
         SET gl_account_code = CASE
             WHEN account_name LIKE '%Payroll%' THEN '1113'
             WHEN account_name LIKE '%Cash%' THEN '1111'
             ELSE '1112'
         END
         WHERE gl_account_code = '' OR gl_account_code IS NULL"
    );
    $pdo->exec(
        "UPDATE cash_accounts
         SET gl_account_code = '1111'
         WHERE gl_account_code = '' OR gl_account_code IS NULL"
    );
    $pdo->exec(
        "UPDATE employees
         SET department_code = CASE
             WHEN department LIKE '%Primary%' THEN 'HR-PRI'
             WHEN department LIKE '%Secondary%' THEN 'HR-SEC'
             WHEN department LIKE '%Finance%' THEN 'HR-FIN'
             WHEN department LIKE '%Transport%' THEN 'HR-TRN'
             ELSE 'HR-ADM'
         END
         WHERE department_code = '' OR department_code IS NULL"
    );
    $pdo->exec(
        "UPDATE employees
         SET job_code = CASE
             WHEN department_code = 'HR-FIN' THEN 'JOB-ACC'
             WHEN department_code = 'HR-TRN' THEN 'JOB-DRV'
             WHEN department_code = 'HR-SEC' THEN 'JOB-SUP'
             WHEN department_code = 'HR-PRI' THEN 'JOB-TCH'
             ELSE 'JOB-PRN'
         END
         WHERE job_code = '' OR job_code IS NULL"
    );
    $pdo->exec("UPDATE hr_attendance SET shift_code = 'SH-DAY' WHERE shift_code = '' OR shift_code IS NULL");
    $pdo->exec(
        "UPDATE hr_leave_requests
         SET leave_type_code = CASE
             WHEN leave_type = 'sick' THEN 'LV-SICK'
             WHEN leave_type = 'emergency' THEN 'LV-EMG'
             WHEN leave_type = 'unpaid' THEN 'LV-UNP'
             ELSE 'LV-ANN'
         END
         WHERE leave_type_code = '' OR leave_type_code IS NULL"
    );
    $pdo->exec("UPDATE sales_invoices SET revenue_account_code = '4100' WHERE revenue_account_code = '' OR revenue_account_code IS NULL");
    $pdo->exec(
        "UPDATE sales_invoices
         SET is_taxable = CASE
             WHEN tax_total > 0 THEN 1
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE sales_invoices
         SET tax_rate_percent = CASE
             WHEN is_taxable = 1 THEN CASE WHEN tax_rate_percent > 0 THEN tax_rate_percent ELSE 15.00 END
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE sales_invoices
         SET payment_method = 'bank',
             payment_account_name = 'Main Operating Account'
         WHERE paid_amount > 0 AND (payment_method = '' OR payment_account_name = '')"
    );
    $pdo->exec(
        "UPDATE sales_invoices
         SET tax_name = CASE
             WHEN is_taxable = 1 AND tax_name = '' THEN 'VAT Sales 15%'
             ELSE tax_name
         END"
    );
    $pdo->exec(
        "UPDATE sales_invoices
         SET inline_paid_amount = CASE
             WHEN payment_method <> '' AND payment_account_name <> '' THEN paid_amount
             ELSE inline_paid_amount
         END
         WHERE inline_paid_amount <= 0 AND paid_amount > 0"
    );
    $pdo->exec("UPDATE purchase_bills SET expense_account_code = '5210' WHERE expense_account_code = '' OR expense_account_code IS NULL");
    $pdo->exec(
        "UPDATE purchase_bills
         SET is_taxable = CASE
             WHEN tax_total > 0 THEN 1
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET tax_rate_percent = CASE
             WHEN is_taxable = 1 THEN CASE WHEN tax_rate_percent > 0 THEN tax_rate_percent ELSE 15.00 END
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET payment_method = 'bank',
             payment_account_name = 'Main Operating Account'
         WHERE paid_amount > 0 AND (payment_method = '' OR payment_account_name = '')"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET tax_name = CASE
             WHEN is_taxable = 1 AND tax_name = '' THEN 'VAT Purchase 15%'
             ELSE tax_name
         END"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET inline_paid_amount = CASE
             WHEN payment_method <> '' AND payment_account_name <> '' THEN paid_amount
             ELSE inline_paid_amount
         END
         WHERE inline_paid_amount <= 0 AND paid_amount > 0"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET withholding_tax_total = ROUND(GREATEST(0, subtotal - discount_total) * (withholding_tax_rate_percent / 100), 2)
         WHERE withholding_tax_name <> '' AND withholding_tax_rate_percent > 0"
    );
    $pdo->exec(
        "UPDATE purchase_bills
         SET net_payable = GREATEST(0, grand_total - withholding_tax_total)
         WHERE net_payable <= 0 OR net_payable IS NULL"
    );
    $pdo->exec("UPDATE expenses SET expense_account_code = '5210' WHERE expense_account_code = '' OR expense_account_code IS NULL");
    $pdo->exec(
        "UPDATE expenses
         SET is_taxable = CASE
             WHEN tax_total > 0 THEN 1
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE expenses
         SET tax_rate_percent = CASE
             WHEN is_taxable = 1 THEN CASE WHEN tax_rate_percent > 0 THEN tax_rate_percent ELSE 15.00 END
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE expenses
         SET payment_method = 'cash',
             payment_account_name = 'Petty Cash Vault'
         WHERE paid_amount > 0 AND (payment_method = '' OR payment_account_name = '')"
    );
    $pdo->exec(
        "UPDATE expenses
         SET tax_name = CASE
             WHEN is_taxable = 1 AND tax_name = '' THEN 'VAT Purchase 15%'
             ELSE tax_name
         END"
    );
    $pdo->exec(
        "UPDATE prepaid_expenses
         SET tax_rate_percent = CASE
             WHEN is_taxable = 1 THEN CASE WHEN tax_rate_percent > 0 THEN tax_rate_percent ELSE 15.00 END
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE prepaid_expenses
         SET tax_name = CASE
             WHEN is_taxable = 1 AND tax_name = '' THEN 'VAT Purchase 15%'
             ELSE tax_name
         END"
    );
    $pdo->exec(
        "UPDATE prepaid_expenses
         SET tax_total = CASE
             WHEN is_taxable = 1 THEN ROUND(GREATEST(total_amount, 0) * (tax_rate_percent / 100), 2)
             ELSE 0
         END"
    );
    $pdo->exec(
        "UPDATE prepaid_expenses
         SET grand_total = ROUND(GREATEST(total_amount, 0) + GREATEST(tax_total, 0), 2)
         WHERE grand_total = 0 OR grand_total IS NULL"
    );
    $pdo->exec(
        "UPDATE expenses
         SET inline_paid_amount = CASE
             WHEN payment_method <> '' AND payment_account_name <> '' THEN paid_amount
             ELSE inline_paid_amount
         END
         WHERE inline_paid_amount <= 0 AND paid_amount > 0"
    );
    $pdo->exec(
        "UPDATE expenses
         SET offset_account_code = ''
         WHERE (grand_total - paid_amount) <= 0.01"
    );
    $pdo->exec(
        "UPDATE payments
         SET offset_account_code = CASE
             WHEN direction = 'out' THEN '2111'
             ELSE '1121'
         END
         WHERE offset_account_code = '' OR offset_account_code IS NULL"
    );
    $pdo->exec(
        "UPDATE payments p
         INNER JOIN sales_invoices s
            ON s.company_id = p.company_id
           AND s.invoice_number = p.reference
         SET p.sales_invoice_id = s.id
         WHERE p.direction = 'in'
           AND (p.sales_invoice_id IS NULL OR p.sales_invoice_id = 0)"
    );
    $pdo->exec(
        "UPDATE payments p
         INNER JOIN purchase_bills b
            ON b.company_id = p.company_id
           AND b.bill_number = p.reference
         SET p.purchase_bill_id = b.id
         WHERE p.direction = 'out'
           AND (p.purchase_bill_id IS NULL OR p.purchase_bill_id = 0)"
    );
    $pdo->exec(
        "UPDATE payments p
         INNER JOIN expenses e
            ON e.company_id = p.company_id
           AND e.expense_number = p.reference
         SET p.expense_id = e.id
         WHERE p.direction = 'out'
           AND (p.expense_id IS NULL OR p.expense_id = 0)"
    );
    $pdo->exec("UPDATE sales_invoices SET cost_center_code = 'CC-ADM' WHERE cost_center_code = '' OR cost_center_code IS NULL");
    $pdo->exec("UPDATE purchase_bills SET cost_center_code = 'CC-ADM' WHERE cost_center_code = '' OR cost_center_code IS NULL");
    $pdo->exec("UPDATE expenses SET cost_center_code = 'CC-ADM' WHERE cost_center_code = '' OR cost_center_code IS NULL");
    $pdo->exec("UPDATE assets SET cost_center_code = 'CC-ADM' WHERE cost_center_code = '' OR cost_center_code IS NULL");
    $pdo->exec("UPDATE tax_rates SET tax_account_code = '2120' WHERE applies_to = 'sales' AND (tax_account_code = '' OR tax_account_code IS NULL)");
    $pdo->exec("UPDATE tax_rates SET tax_account_code = '1140' WHERE applies_to = 'purchase' AND (tax_account_code = '' OR tax_account_code IS NULL)");
    $pdo->exec("UPDATE tax_rates SET tax_account_code = '2160' WHERE applies_to = 'withholding' AND (tax_account_code = '' OR tax_account_code IS NULL)");
    $pdo->exec(
        "UPDATE journal_entry_lines l
         INNER JOIN chart_of_accounts c
           ON c.company_id = l.company_id
          AND c.account_code = l.account_code
         SET l.cost_center_code = 'CC-ADM'
         WHERE (l.cost_center_code = '' OR l.cost_center_code IS NULL)
           AND c.account_type IN ('revenue', 'expense')"
    );

    $companyName = app_normalize_whitespace((string) app_config('default_company_name', 'Orion Business Group'));
    $currency = app_normalize_whitespace((string) app_config('default_company_currency', 'SAR'));

    $companyStmt = $pdo->prepare('SELECT id FROM companies WHERE name = :name LIMIT 1');
    $companyStmt->execute(['name' => $companyName]);
    $companyId = $companyStmt->fetchColumn();
    if (!$companyId) {
        $insertCompany = $pdo->prepare(
            'INSERT INTO companies (name, legal_name, vat_number, logo_path, currency_code, fiscal_year_start, is_active)
             VALUES (:name, :legal_name, :vat_number, :logo_path, :currency_code, :fiscal_year_start, 1)'
        );
        $insertCompany->execute([
            'name' => $companyName,
            'legal_name' => $companyName,
            'vat_number' => '300000000000003',
            'logo_path' => 'assets/logo.svg',
            'currency_code' => $currency !== '' ? $currency : 'SAR',
            'fiscal_year_start' => '01-01',
        ]);
        $companyId = (int) $pdo->lastInsertId();
    } else {
        $companyId = (int) $companyId;
    }

    app_seed_theme_defaults($pdo);
    app_seed_default_role_permissions($pdo);

    $usersCount = (int) ($pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() ?: 0);
    if ($usersCount === 0) {
        $insertUser = $pdo->prepare(
            'INSERT INTO users (username, password_hash, full_name, role, company_id, is_active)
             VALUES (:username, :password_hash, :full_name, :role, :company_id, 1)'
        );
        $insertUser->execute([
            'username' => (string) app_config('default_admin_username', 'admin'),
            'password_hash' => password_hash((string) app_config('default_admin_password', 'Admin@12345'), PASSWORD_DEFAULT),
            'full_name' => 'System Administrator',
            'role' => 'admin',
            'company_id' => $companyId,
        ]);
    }

    $pdo->exec(
        'INSERT IGNORE INTO user_company_access (user_id, company_id)
         SELECT id, company_id
         FROM users
         WHERE company_id IS NOT NULL
           AND company_id > 0'
    );
    $pdo->exec(
        "INSERT IGNORE INTO student_account_settings
         (company_id, receivable_account_code, advance_account_code, discount_account_code, default_revenue_account_code, default_cost_center_code)
         SELECT id, '1121', '2140', '2140', '4100', 'CC-ADM'
         FROM companies"
    );
    $pdo->exec(
        "INSERT IGNORE INTO posting_account_settings
         (company_id, sales_receivable_account_code, purchase_payable_account_code, accrued_expense_account_code, prepaid_expense_account_code,
         ifrs16_rou_asset_account_code, ifrs16_lease_liability_account_code, ifrs16_interest_expense_account_code,
          ifrs16_depreciation_expense_account_code, ifrs16_accumulated_depreciation_account_code, ifrs16_payment_account_code,
          ifrs16_restoration_provision_account_code, ifrs16_lease_incentive_account_code,
          pos_sales_revenue_account_code, pos_cash_over_short_gain_account_code, pos_cash_over_short_loss_account_code)
         SELECT id, '1121', '2111', '2130', '1150', '1231', '2210', '5700', '5600', '1239', '1112', '2220', '1160', '4100', '4100', '5510'
         FROM companies"
    );

    foreach (($pdo->query('SELECT id FROM companies')->fetchAll(PDO::FETCH_COLUMN) ?: []) as $existingCompanyId) {
        app_normalize_document_expense_accounts($pdo, (int) $existingCompanyId);
        app_normalize_expense_offset_accounts($pdo, (int) $existingCompanyId);
    }

    if (app_demo_seed_enabled() && !app_seed_has_run($pdo, 'demo_data', $companyId)) {
        app_seed_demo_data($pdo, $companyId);
        app_seed_mark_run($pdo, 'demo_data', $companyId);
        $targetCompanyStmt = $pdo->prepare('SELECT id FROM companies WHERE id <> :default_company_id ORDER BY id ASC');
        $targetCompanyStmt->execute(['default_company_id' => $companyId]);
        foreach (($targetCompanyStmt->fetchAll(PDO::FETCH_COLUMN) ?: []) as $targetCompanyId) {
            $targetCompanyId = (int) $targetCompanyId;
            if (app_company_is_excellence_accounting_target($pdo, $targetCompanyId) && !app_seed_has_run($pdo, 'demo_data', $targetCompanyId)) {
                app_seed_demo_data($pdo, $targetCompanyId);
                app_seed_mark_run($pdo, 'demo_data', $targetCompanyId);
            }
        }
    }
    foreach (($pdo->query('SELECT id FROM companies ORDER BY id ASC')->fetchAll(PDO::FETCH_COLUMN) ?: []) as $targetCompanyId) {
        app_seed_ecl_defaults_from_company($pdo, $companyId, (int) $targetCompanyId);
        app_seed_ecl_country_defaults($pdo, (int) $targetCompanyId);
    }
}

function app_column_exists(PDO $pdo, string $table, string $column): bool
{
    $stmt = $pdo->prepare(
        'SELECT 1
         FROM information_schema.columns
         WHERE table_schema = DATABASE()
           AND table_name = :table
           AND column_name = :column
         LIMIT 1'
    );
    $stmt->execute([
        'table' => $table,
        'column' => $column,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_add_column_if_missing(PDO $pdo, string $table, string $column, string $definition): void
{
    if (!app_identifier($table) || !app_identifier($column)) {
        throw new RuntimeException('Invalid migration identifier.');
    }
    if (app_column_exists($pdo, $table, $column)) {
        return;
    }
    $pdo->exec('ALTER TABLE `' . $table . '` ADD COLUMN `' . $column . '` ' . $definition);
}

function app_index_exists(PDO $pdo, string $table, string $indexName): bool
{
    $stmt = $pdo->prepare(
        'SELECT 1
         FROM information_schema.statistics
         WHERE table_schema = DATABASE()
           AND table_name = :table
           AND index_name = :index_name
         LIMIT 1'
    );
    $stmt->execute([
        'table' => $table,
        'index_name' => $indexName,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_demo_seed_enabled(): bool
{
    $configEnabled = in_array(strtolower(app_normalize_whitespace((string) app_config('seed_demo_data', false))), ['1', 'true', 'yes', 'on'], true);
    if (!$configEnabled) {
        return false;
    }

    $value = getenv('ORION_SEED_DEMO_DATA');
    if ($value === false || $value === '') {
        return true;
    }

    return in_array(strtolower(app_normalize_whitespace((string) $value)), ['1', 'true', 'yes', 'on'], true);
}

function app_seed_run_state_table(PDO $pdo): void
{
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS app_seed_runs (
            company_id INT UNSIGNED NOT NULL,
            seed_name VARCHAR(80) NOT NULL,
            seeded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (company_id, seed_name)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
    );
}

function app_seed_has_run(PDO $pdo, string $seedName, int $companyId): bool
{
    app_seed_run_state_table($pdo);
    $stmt = $pdo->prepare(
        'SELECT 1
         FROM app_seed_runs
         WHERE company_id = :company_id
           AND seed_name = :seed_name
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'seed_name' => $seedName,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_seed_mark_run(PDO $pdo, string $seedName, int $companyId): void
{
    app_seed_run_state_table($pdo);
    $stmt = $pdo->prepare(
        'INSERT IGNORE INTO app_seed_runs (company_id, seed_name)
         VALUES (:company_id, :seed_name)'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'seed_name' => $seedName,
    ]);
}

function app_seed_default_roles(PDO $pdo): void
{
    $roles = [
        ['admin', 'Administrator', 'Full access to every module and setup screen.'],
        ['accountant', 'Accountant', 'Daily accounting user with operational posting access.'],
        ['auditor', 'Auditor', 'Read-only review access without posting or editing.'],
    ];

    $stmt = $pdo->prepare(
        'INSERT IGNORE INTO roles (role_code, role_name, description, is_system, is_active)
         VALUES (:role_code, :role_name, :description, 1, 1)'
    );
    foreach ($roles as [$roleCode, $roleName, $description]) {
        $stmt->execute([
            'role_code' => $roleCode,
            'role_name' => $roleName,
            'description' => $description,
        ]);
    }
}

function app_seed_default_role_permissions(PDO $pdo): void
{
    app_seed_default_roles($pdo);

    $stmt = $pdo->prepare(
        'INSERT IGNORE INTO role_permissions
         (role_code, module_slug, can_access, can_create, can_edit, can_delete, can_import, can_post)
         VALUES
         (:role_code, :module_slug, :can_access, :can_create, :can_edit, :can_delete, :can_import, :can_post)'
    );

    foreach (['admin', 'accountant', 'auditor'] as $roleCode) {
        foreach (array_keys(app_module_definitions()) as $moduleSlug) {
            $permission = app_role_default_permission($roleCode, (string) $moduleSlug);
            $stmt->execute([
                'role_code' => $roleCode,
                'module_slug' => (string) $moduleSlug,
                'can_access' => (int) ($permission['can_access'] ?? 0),
                'can_create' => (int) ($permission['can_create'] ?? 0),
                'can_edit' => (int) ($permission['can_edit'] ?? 0),
                'can_delete' => (int) ($permission['can_delete'] ?? 0),
                'can_import' => (int) ($permission['can_import'] ?? 0),
                'can_post' => (int) ($permission['can_post'] ?? 0),
            ]);
        }
    }
}

function app_seed_table_count(PDO $pdo, string $table, ?int $companyId = null): int
{
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid seed table name.');
    }

    $sql = 'SELECT COUNT(*) FROM `' . $table . '`';
    $params = [];
    if ($companyId !== null) {
        $sql .= ' WHERE company_id = :company_id';
        $params['company_id'] = $companyId;
    }

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return (int) ($stmt->fetchColumn() ?: 0);
}

function app_seed_insert_rows(PDO $pdo, string $table, array $rows): void
{
    if (!$rows) {
        return;
    }
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid seed table name.');
    }

    $columns = array_keys($rows[0]);
    foreach ($columns as $column) {
        if (!app_identifier((string) $column)) {
            throw new RuntimeException('Invalid seed column name.');
        }
    }

    $quotedColumns = implode(', ', array_map(static fn (string $column): string => '`' . $column . '`', $columns));
    $placeholders = implode(', ', array_map(static fn (string $column): string => ':' . $column, $columns));
    $sql = 'INSERT IGNORE INTO `' . $table . '` (' . $quotedColumns . ') VALUES (' . $placeholders . ')';
    $stmt = $pdo->prepare($sql);

    foreach ($rows as $row) {
        $params = [];
        foreach ($columns as $column) {
            $params[$column] = $row[$column] ?? null;
        }
        $stmt->execute($params);
    }
}

function app_seed_theme_defaults(PDO $pdo): void
{
    app_seed_insert_rows($pdo, 'themes', [
        [
            'theme_code' => 'ORION_LIGHT',
            'theme_name' => 'Original UI - Light',
            'ui_skin' => 'classic',
            'color_mode' => 'light',
            'is_default' => 0,
            'is_active' => 1,
            'notes' => 'Original Orion interface with light colors.',
        ],
        [
            'theme_code' => 'ORION_DARK',
            'theme_name' => 'Original UI - Dark',
            'ui_skin' => 'classic',
            'color_mode' => 'dark',
            'is_default' => 0,
            'is_active' => 1,
            'notes' => 'Original Orion interface with dark colors.',
        ],
        [
            'theme_code' => 'DEVELOP_LIGHT',
            'theme_name' => 'Develop UI - Light',
            'ui_skin' => 'finance',
            'color_mode' => 'light',
            'is_default' => 1,
            'is_active' => 1,
            'notes' => 'Dashboard-style finance interface inspired by the provided light reference.',
        ],
        [
            'theme_code' => 'DEVELOP_DARK',
            'theme_name' => 'Develop UI - Dark',
            'ui_skin' => 'finance',
            'color_mode' => 'dark',
            'is_default' => 0,
            'is_active' => 1,
            'notes' => 'Dashboard-style finance interface inspired by the provided dark reference.',
        ],
    ]);

    $defaultCount = (int) ($pdo->query('SELECT COUNT(*) FROM themes WHERE is_default = 1 AND is_active = 1')->fetchColumn() ?: 0);
    if ($defaultCount === 0) {
        $pdo->exec("UPDATE themes SET is_default = CASE WHEN theme_code = 'DEVELOP_LIGHT' THEN 1 ELSE 0 END");
    }
}

function app_apply_ui_theme_record(array $record): void
{
    $skin = app_normalize_ui_skin((string) ($record['ui_skin'] ?? 'finance'));
    $theme = app_normalize_ui_mode((string) ($record['color_mode'] ?? 'light'));
    $_SESSION['ui_skin'] = $skin;
    $_SESSION['ui_theme'] = $theme;

    if (!headers_sent()) {
        setcookie('orion_skin', $skin, time() + 31536000, '/');
        setcookie('orion_theme', $theme, time() + 31536000, '/');
    }
}

function app_ecl_default_country_seed_rows(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $path = __DIR__ . '/data/ecl_country_defaults.tsv';
    if (!is_file($path)) {
        return [];
    }

    $ratingDefaults = [
        'EGYPT' => ['B', 'B2', 'B+', 'Seeded from the ECL workbook macro scenario and Egypt sovereign ratings.'],
        'SAUDI_ARABIA' => ['A+', 'Aa3', 'A+', 'Seeded from the ECL workbook macro scenario; Saudi Arabia is the default ECL country.'],
    ];

    $rows = [];
    $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
    foreach (array_slice($lines, 1) as $line) {
        $columns = array_pad(explode("\t", $line), 5, '');
        $countryCode = app_normalize_whitespace((string) $columns[0]);
        $countryName = app_normalize_whitespace((string) $columns[1]);
        if ($countryCode === '' || $countryName === '') {
            continue;
        }

        $ratings = $ratingDefaults[$countryCode] ?? ['', '', '', 'Seeded from the ECL workbook macro scenario data.'];
        $rows[] = [
            'company_id' => $companyId,
            'country_code' => $countryCode,
            'country_name' => $countryName,
            'sp_rating' => $ratings[0],
            'moodys_rating' => $ratings[1],
            'fitch_rating' => $ratings[2],
            'base_scenario_factor' => app_number($columns[2] ?? 0),
            'best_scenario_factor' => app_number($columns[3] ?? 0),
            'worst_scenario_factor' => app_number($columns[4] ?? 0),
            'base_weight_percent' => 60,
            'best_weight_percent' => 10,
            'worst_weight_percent' => 30,
            'is_active' => 1,
            'notes' => $ratings[3],
        ];
    }

    return $rows;
}

function app_seed_ecl_country_defaults(PDO $pdo, int $companyId): void
{
    app_seed_insert_rows($pdo, 'ecl_countries', app_ecl_default_country_seed_rows($companyId));
}

function app_seed_ecl_defaults_from_company(PDO $pdo, int $sourceCompanyId, int $targetCompanyId): void
{
    if ($sourceCompanyId <= 0 || $targetCompanyId <= 0) {
        return;
    }

    $tables = [
        'ecl_countries' => [
            'country_code', 'country_name', 'sp_rating', 'moodys_rating', 'fitch_rating',
            'base_scenario_factor', 'best_scenario_factor', 'worst_scenario_factor',
            'base_weight_percent', 'best_weight_percent', 'worst_weight_percent',
            'is_active', 'notes',
        ],
        'ecl_default_rates' => ['agency', 'rating_code', 'one_year_pd_percent', 'is_active'],
        'ecl_receivable_buckets' => ['bucket_code', 'bucket_name', 'min_days', 'max_days', 'loss_rate_percent', 'sort_order', 'is_active'],
        'ecl_report_categories' => [
            'category_code', 'category_name', 'source_type', 'loss_model', 'country_code',
            'account_codes', 'financial_statement_group_code', 'manual_amount', 'lgd_percent',
            'rate_override_percent', 'sort_order', 'is_active', 'notes',
        ],
    ];

    foreach ($tables as $table => $columns) {
        if (app_seed_table_count($pdo, $table, $targetCompanyId) > 0) {
            continue;
        }
        $quotedColumns = implode(', ', array_map(static fn (string $column): string => '`' . $column . '`', $columns));
        $stmt = $pdo->prepare('SELECT ' . $quotedColumns . ' FROM `' . $table . '` WHERE company_id = :company_id ORDER BY id ASC');
        $stmt->execute(['company_id' => $sourceCompanyId]);
        $rows = [];
        foreach (($stmt->fetchAll() ?: []) as $row) {
            $seedRow = ['company_id' => $targetCompanyId];
            foreach ($columns as $column) {
                $seedRow[$column] = $row[$column] ?? null;
            }
            $rows[] = $seedRow;
        }
        app_seed_insert_rows($pdo, $table, $rows);
    }
}

function app_seed_upsert_chart_accounts(PDO $pdo, array $rows): void
{
    if (!$rows) {
        return;
    }

    $stmt = $pdo->prepare(
        'INSERT INTO chart_of_accounts
         (company_id, account_code, account_name, account_type, parent_code, opening_balance, is_active)
         VALUES
         (:company_id, :account_code, :account_name, :account_type, :parent_code, :opening_balance, :is_active)
         ON DUPLICATE KEY UPDATE
             account_name = VALUES(account_name),
             account_type = VALUES(account_type),
             parent_code = VALUES(parent_code),
             is_active = VALUES(is_active)'
    );

    foreach ($rows as $row) {
        $stmt->execute([
            'company_id' => (int) ($row['company_id'] ?? 0),
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'account_type' => (string) ($row['account_type'] ?? ''),
            'parent_code' => (string) ($row['parent_code'] ?? ''),
            'opening_balance' => app_number($row['opening_balance'] ?? 0),
            'is_active' => (int) ($row['is_active'] ?? 1),
        ]);
    }
}

function app_seed_tax_rate_if_missing(PDO $pdo, int $companyId, string $taxName, float $ratePercent, string $appliesTo, string $accountCode): void
{
    if ($companyId <= 0 || $taxName === '') {
        return;
    }

    $select = $pdo->prepare(
        'SELECT id
         FROM tax_rates
         WHERE company_id = :company_id AND tax_name = :tax_name
         ORDER BY id ASC
         LIMIT 1'
    );
    $select->execute([
        'company_id' => $companyId,
        'tax_name' => $taxName,
    ]);
    $id = (int) ($select->fetchColumn() ?: 0);

    if ($id > 0) {
        $update = $pdo->prepare(
            'UPDATE tax_rates
             SET rate_percent = :rate_percent,
                 applies_to = :applies_to,
                 tax_account_code = :tax_account_code,
                 is_active = 1
             WHERE id = :id AND company_id = :company_id'
        );
        $update->execute([
            'rate_percent' => $ratePercent,
            'applies_to' => $appliesTo,
            'tax_account_code' => $accountCode,
            'id' => $id,
            'company_id' => $companyId,
        ]);
        return;
    }

    $insert = $pdo->prepare(
        'INSERT INTO tax_rates
         (company_id, tax_name, rate_percent, applies_to, tax_account_code, is_active)
         VALUES
         (:company_id, :tax_name, :rate_percent, :applies_to, :tax_account_code, 1)'
    );
    $insert->execute([
        'company_id' => $companyId,
        'tax_name' => $taxName,
        'rate_percent' => $ratePercent,
        'applies_to' => $appliesTo,
        'tax_account_code' => $accountCode,
    ]);
}

function app_company_is_excellence_accounting_target(PDO $pdo, int $companyId): bool
{
    if ($companyId <= 0) {
        return false;
    }

    $stmt = $pdo->prepare('SELECT name, legal_name FROM companies WHERE id = :id LIMIT 1');
    $stmt->execute(['id' => $companyId]);
    $company = $stmt->fetch();
    if (!$company) {
        return false;
    }

    $haystack = (string) ($company['name'] ?? '') . ' ' . (string) ($company['legal_name'] ?? '');
    return stripos($haystack, 'excellence') !== false
        || strpos($haystack, 'EXcellence') !== false
        || strpos($haystack, 'إكسلنس') !== false
        || strpos($haystack, 'اكسلنس') !== false
        || strpos($haystack, 'المركز المالي') !== false;
}

function app_normalize_document_expense_accounts(PDO $pdo, int $companyId): void
{
    if ($companyId <= 0) {
        return;
    }

    $fallbackCode = app_account_code_by_type($companyId, 'expense', ['5210', '5330', '5410', '5510', '5100', '5600']);
    if ($fallbackCode === null || $fallbackCode === '') {
        return;
    }

    foreach (['purchase_bills', 'expenses'] as $table) {
        $stmt = $pdo->prepare(
            'UPDATE `' . $table . '` d
             LEFT JOIN chart_of_accounts c
               ON c.company_id = d.company_id
              AND c.account_code = d.expense_account_code
              AND c.account_type = :account_type
              AND c.is_active = 1
             LEFT JOIN chart_of_accounts child
               ON child.company_id = c.company_id
              AND child.parent_code = c.account_code
             SET d.expense_account_code = :fallback_code
             WHERE d.company_id = :company_id
               AND (
                   d.expense_account_code = \'\'
                   OR d.expense_account_code IS NULL
                   OR c.account_code IS NULL
                   OR child.account_code IS NOT NULL
               )'
        );
        $stmt->execute([
            'account_type' => 'expense',
            'fallback_code' => $fallbackCode,
            'company_id' => $companyId,
        ]);
    }
}

function app_normalize_expense_offset_accounts(PDO $pdo, int $companyId): void
{
    if ($companyId <= 0) {
        return;
    }

    $pdo->prepare(
        'UPDATE expenses
         SET offset_account_code = \'\'
         WHERE company_id = :company_id
           AND (grand_total - paid_amount) <= 0.01'
    )->execute([
        'company_id' => $companyId,
    ]);

    $fallbackCode = app_account_code_by_type($companyId, 'liability', ['2111', '2100', '2130']);
    if ($fallbackCode === null || $fallbackCode === '') {
        return;
    }

    $stmt = $pdo->prepare(
        'UPDATE expenses d
         LEFT JOIN chart_of_accounts c
           ON c.company_id = d.company_id
          AND c.account_code = d.offset_account_code
          AND c.account_type = :account_type
          AND c.is_active = 1
         LEFT JOIN chart_of_accounts child
           ON child.company_id = c.company_id
          AND child.parent_code = c.account_code
         SET d.offset_account_code = :fallback_code
         WHERE d.company_id = :company_id
           AND (d.grand_total - d.paid_amount) > 0.01
           AND (
               d.offset_account_code = \'\'
               OR d.offset_account_code IS NULL
               OR c.account_code IS NULL
               OR child.account_code IS NOT NULL
           )'
    );
    $stmt->execute([
        'account_type' => 'liability',
        'fallback_code' => $fallbackCode,
        'company_id' => $companyId,
    ]);
}

function app_seed_demo_data(PDO $pdo, int $companyId): void
{
    $year = (int) date('Y');
    $today = date('Y-m-d');
    $isExcellenceTarget = app_company_is_excellence_accounting_target($pdo, $companyId);

    if (app_seed_table_count($pdo, 'chart_of_accounts', $companyId) < 35) {
        app_seed_insert_rows($pdo, 'chart_of_accounts', [
            ['company_id' => $companyId, 'account_code' => '1000', 'account_name' => 'Assets', 'account_type' => 'asset', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1100', 'account_name' => 'Current Assets', 'account_type' => 'asset', 'parent_code' => '1000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1110', 'account_name' => 'Cash and Cash Equivalents', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1111', 'account_name' => 'Cash on Hand', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 15000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1112', 'account_name' => 'Bank - Main Operating', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 250000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1113', 'account_name' => 'Bank - Payroll', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 120000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1120', 'account_name' => 'Accounts Receivable', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1121', 'account_name' => 'Accounts Receivable - Students', 'account_type' => 'asset', 'parent_code' => '1120', 'opening_balance' => 27500, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1130', 'account_name' => 'Inventory', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1131', 'account_name' => 'Inventory - Books', 'account_type' => 'asset', 'parent_code' => '1130', 'opening_balance' => 38000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1132', 'account_name' => 'Inventory - Uniforms', 'account_type' => 'asset', 'parent_code' => '1130', 'opening_balance' => 21000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1140', 'account_name' => 'VAT Input Recoverable', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1150', 'account_name' => 'Prepaid Expenses', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 5000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1200', 'account_name' => 'Non Current Assets', 'account_type' => 'asset', 'parent_code' => '1000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1210', 'account_name' => 'Property and Equipment', 'account_type' => 'asset', 'parent_code' => '1200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1211', 'account_name' => 'School Buildings', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 1800000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1212', 'account_name' => 'Furniture and Fixtures', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 360000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1213', 'account_name' => 'Computers and Lab Equipment', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 240000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1214', 'account_name' => 'School Vehicles', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 820000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1220', 'account_name' => 'Accumulated Depreciation', 'account_type' => 'asset', 'parent_code' => '1200', 'opening_balance' => -120000, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '2000', 'account_name' => 'Liabilities', 'account_type' => 'liability', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2100', 'account_name' => 'Current Liabilities', 'account_type' => 'liability', 'parent_code' => '2000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2110', 'account_name' => 'Accounts Payable', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2111', 'account_name' => 'Accounts Payable - Vendors', 'account_type' => 'liability', 'parent_code' => '2110', 'opening_balance' => 22000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2120', 'account_name' => 'VAT Output Payable', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2130', 'account_name' => 'Accrued Expenses', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 14000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2140', 'account_name' => 'Unearned Tuition Revenue', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 30000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2200', 'account_name' => 'Non Current Liabilities', 'account_type' => 'liability', 'parent_code' => '2000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2210', 'account_name' => 'Lease Obligations', 'account_type' => 'liability', 'parent_code' => '2200', 'opening_balance' => 180000, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '3000', 'account_name' => 'Equity', 'account_type' => 'equity', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3100', 'account_name' => 'Owner Capital', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 2200000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3200', 'account_name' => 'Retained Earnings', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 340000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3300', 'account_name' => 'Current Year Earnings', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 0, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '4000', 'account_name' => 'Operating Revenue', 'account_type' => 'revenue', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4100', 'account_name' => 'Tuition Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4110', 'account_name' => 'Registration Fees Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4120', 'account_name' => 'Transport Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4130', 'account_name' => 'Books Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4140', 'account_name' => 'Uniform Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4200', 'account_name' => 'Other Revenue', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4210', 'account_name' => 'Activities Revenue', 'account_type' => 'revenue', 'parent_code' => '4200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4220', 'account_name' => 'Late Fee Revenue', 'account_type' => 'revenue', 'parent_code' => '4200', 'opening_balance' => 0, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '5000', 'account_name' => 'Operating Expenses', 'account_type' => 'expense', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5100', 'account_name' => 'Salaries and Wages', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5110', 'account_name' => 'Staff Benefits', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5200', 'account_name' => 'Academic Expenses', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5210', 'account_name' => 'Teaching Materials Expense', 'account_type' => 'expense', 'parent_code' => '5200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5220', 'account_name' => 'Examination Expense', 'account_type' => 'expense', 'parent_code' => '5200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5300', 'account_name' => 'Facility Expenses', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5310', 'account_name' => 'Rent Expense', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5320', 'account_name' => 'Utilities Expense', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5330', 'account_name' => 'Maintenance Expense', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5400', 'account_name' => 'Transport Expenses', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5410', 'account_name' => 'Bus Fuel Expense', 'account_type' => 'expense', 'parent_code' => '5400', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5420', 'account_name' => 'Bus Maintenance Expense', 'account_type' => 'expense', 'parent_code' => '5400', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5500', 'account_name' => 'Administrative Expenses', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5510', 'account_name' => 'Office Supplies Expense', 'account_type' => 'expense', 'parent_code' => '5500', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5520', 'account_name' => 'Communication Expense', 'account_type' => 'expense', 'parent_code' => '5500', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5530', 'account_name' => 'Professional Fees', 'account_type' => 'expense', 'parent_code' => '5500', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5600', 'account_name' => 'Depreciation Expense', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5700', 'account_name' => 'Finance Costs', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
        ]);
    }

    app_seed_upsert_chart_accounts($pdo, [
        ['company_id' => $companyId, 'account_code' => '1160', 'account_name' => 'Deposits and Lease Incentives Receivable', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
        ['company_id' => $companyId, 'account_code' => '1230', 'account_name' => 'Right-of-Use Assets', 'account_type' => 'asset', 'parent_code' => '1200', 'opening_balance' => 0, 'is_active' => 1],
        ['company_id' => $companyId, 'account_code' => '1231', 'account_name' => 'Right-of-Use Asset - Leases', 'account_type' => 'asset', 'parent_code' => '1230', 'opening_balance' => 0, 'is_active' => 1],
        ['company_id' => $companyId, 'account_code' => '1239', 'account_name' => 'Accumulated Depreciation - Right-of-Use Assets', 'account_type' => 'asset', 'parent_code' => '1230', 'opening_balance' => 0, 'is_active' => 1],
        ['company_id' => $companyId, 'account_code' => '2220', 'account_name' => 'Restoration and Dismantling Provision', 'account_type' => 'liability', 'parent_code' => '2200', 'opening_balance' => 0, 'is_active' => 1],
    ]);

    if (app_seed_table_count($pdo, 'contacts', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'contacts', [
            ['company_id' => $companyId, 'contact_type' => 'customer', 'contact_code' => 'CUST001', 'contact_name' => 'Parent - Ahmed Ali', 'tax_number' => '', 'phone' => '0501001001', 'email' => 'parent1@example.com', 'address' => 'Riyadh', 'opening_balance' => 1200, 'is_active' => 1],
            ['company_id' => $companyId, 'contact_type' => 'customer', 'contact_code' => 'CUST002', 'contact_name' => 'Parent - Sara Omar', 'tax_number' => '', 'phone' => '0501001002', 'email' => 'parent2@example.com', 'address' => 'Riyadh', 'opening_balance' => 840, 'is_active' => 1],
            ['company_id' => $companyId, 'contact_type' => 'customer', 'contact_code' => 'CUST003', 'contact_name' => 'Parent - Khaled Sami', 'tax_number' => '', 'phone' => '0501001003', 'email' => 'parent3@example.com', 'address' => 'Riyadh', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'contact_type' => 'vendor', 'contact_code' => 'VEND001', 'contact_name' => 'School Supplies Co.', 'tax_number' => '300112233400003', 'phone' => '0509002001', 'email' => 'vendor1@example.com', 'address' => 'Jeddah', 'opening_balance' => 2500, 'is_active' => 1],
            ['company_id' => $companyId, 'contact_type' => 'vendor', 'contact_code' => 'VEND002', 'contact_name' => 'Bus Services Ltd.', 'tax_number' => '300112233400004', 'phone' => '0509002002', 'email' => 'vendor2@example.com', 'address' => 'Dammam', 'opening_balance' => 1800, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'tax_rates', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'tax_rates', [
            ['company_id' => $companyId, 'tax_name' => 'VAT Sales 15%', 'rate_percent' => 15, 'applies_to' => 'sales', 'tax_account_code' => '2120', 'is_active' => 1],
            ['company_id' => $companyId, 'tax_name' => 'VAT Purchase 15%', 'rate_percent' => 15, 'applies_to' => 'purchase', 'tax_account_code' => '1140', 'is_active' => 1],
            ['company_id' => $companyId, 'tax_name' => 'Zero Rated', 'rate_percent' => 0, 'applies_to' => 'both', 'tax_account_code' => '', 'is_active' => 1],
            ['company_id' => $companyId, 'tax_name' => 'Exempt', 'rate_percent' => 0, 'applies_to' => 'both', 'tax_account_code' => '', 'is_active' => 1],
            ['company_id' => $companyId, 'tax_name' => 'Service Fee Tax 5%', 'rate_percent' => 5, 'applies_to' => 'both', 'tax_account_code' => '', 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'products', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'products', [
            ['company_id' => $companyId, 'sku' => 'TUITION-G1', 'product_name' => 'Tuition Fee - Grade 1', 'category' => 'Tuition', 'unit_name' => 'Student', 'sale_price' => 12500, 'purchase_price' => 0, 'qty_on_hand' => 0, 'tax_rate_percent' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'sku' => 'TUITION-G2', 'product_name' => 'Tuition Fee - Grade 2', 'category' => 'Tuition', 'unit_name' => 'Student', 'sale_price' => 13200, 'purchase_price' => 0, 'qty_on_hand' => 0, 'tax_rate_percent' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'sku' => 'TRANSPORT', 'product_name' => 'School Transport Package', 'category' => 'Services', 'unit_name' => 'Student', 'sale_price' => 3200, 'purchase_price' => 2200, 'qty_on_hand' => 0, 'tax_rate_percent' => 15, 'is_active' => 1],
            ['company_id' => $companyId, 'sku' => 'BOOKS-SET', 'product_name' => 'Books and Worksheets Set', 'category' => 'Books', 'unit_name' => 'Set', 'sale_price' => 850, 'purchase_price' => 500, 'qty_on_hand' => 120, 'tax_rate_percent' => 15, 'is_active' => 1],
            ['company_id' => $companyId, 'sku' => 'UNIFORM-SET', 'product_name' => 'Uniform Set', 'category' => 'Uniform', 'unit_name' => 'Set', 'sale_price' => 620, 'purchase_price' => 380, 'qty_on_hand' => 90, 'tax_rate_percent' => 15, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'ecl_countries', $companyId) === 0) {
        app_seed_insert_rows($pdo, 'ecl_countries', [
            [
                'company_id' => $companyId,
                'country_code' => 'EGYPT',
                'country_name' => 'Egypt',
                'sp_rating' => 'B',
                'moodys_rating' => 'B2',
                'fitch_rating' => 'B+',
                'base_scenario_factor' => 0.05230888,
                'best_scenario_factor' => 0.96290387,
                'worst_scenario_factor' => -0.85828611,
                'base_weight_percent' => 60,
                'best_weight_percent' => 10,
                'worst_weight_percent' => 30,
                'is_active' => 1,
                'notes' => 'Seeded from the ECL workbook macro scenario and Egypt sovereign ratings.',
            ],
        ]);
    }
    app_seed_ecl_country_defaults($pdo, $companyId);

    if (app_seed_table_count($pdo, 'ecl_default_rates', $companyId) === 0) {
        app_seed_insert_rows($pdo, 'ecl_default_rates', [
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'AAA', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'AA+', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'AA', 'one_year_pd_percent' => 0.020000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'AA-', 'one_year_pd_percent' => 0.030000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'A+', 'one_year_pd_percent' => 0.050000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'A', 'one_year_pd_percent' => 0.050000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'A-', 'one_year_pd_percent' => 0.060000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BBB+', 'one_year_pd_percent' => 0.090000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BBB', 'one_year_pd_percent' => 0.150000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BBB-', 'one_year_pd_percent' => 0.240000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BB+', 'one_year_pd_percent' => 0.320000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BB', 'one_year_pd_percent' => 0.480000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'BB-', 'one_year_pd_percent' => 0.960000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'B+', 'one_year_pd_percent' => 1.980000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'B', 'one_year_pd_percent' => 3.130000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'B-', 'one_year_pd_percent' => 6.520000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'CCC/C', 'one_year_pd_percent' => 28.300000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'sp', 'rating_code' => 'D', 'one_year_pd_percent' => 100.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Aaa', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Aa1', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Aa2', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Aa3', 'one_year_pd_percent' => 0.050000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'A1', 'one_year_pd_percent' => 0.090000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'A2', 'one_year_pd_percent' => 0.060000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'A3', 'one_year_pd_percent' => 0.070000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Baa1', 'one_year_pd_percent' => 0.120000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Baa2', 'one_year_pd_percent' => 0.160000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Baa3', 'one_year_pd_percent' => 0.230000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Ba1', 'one_year_pd_percent' => 0.290000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Ba2', 'one_year_pd_percent' => 0.620000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Ba3', 'one_year_pd_percent' => 0.830000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'B1', 'one_year_pd_percent' => 1.180000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'B2', 'one_year_pd_percent' => 2.680000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'B3', 'one_year_pd_percent' => 3.480000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Caa1', 'one_year_pd_percent' => 4.400000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Caa2', 'one_year_pd_percent' => 8.100000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Caa3', 'one_year_pd_percent' => 19.570000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'Ca-C', 'one_year_pd_percent' => 36.900000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'moodys', 'rating_code' => 'C', 'one_year_pd_percent' => 100.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'AAA', 'one_year_pd_percent' => 0.120000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'AA+', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'AA', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'AA-', 'one_year_pd_percent' => 0.080000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'A+', 'one_year_pd_percent' => 0.000000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'A', 'one_year_pd_percent' => 0.070000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'A-', 'one_year_pd_percent' => 0.060000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BBB+', 'one_year_pd_percent' => 0.080000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BBB', 'one_year_pd_percent' => 0.050000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BBB-', 'one_year_pd_percent' => 0.250000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BB+', 'one_year_pd_percent' => 0.280000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BB', 'one_year_pd_percent' => 0.520000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'BB-', 'one_year_pd_percent' => 1.090000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'B+', 'one_year_pd_percent' => 1.410000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'B', 'one_year_pd_percent' => 2.100000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'B-', 'one_year_pd_percent' => 3.200000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'CCC/C', 'one_year_pd_percent' => 23.550000, 'is_active' => 1],
            ['company_id' => $companyId, 'agency' => 'fitch', 'rating_code' => 'D', 'one_year_pd_percent' => 100.000000, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'ecl_receivable_buckets', $companyId) === 0) {
        app_seed_insert_rows($pdo, 'ecl_receivable_buckets', [
            ['company_id' => $companyId, 'bucket_code' => 'CURRENT', 'bucket_name' => 'Current', 'min_days' => 0, 'max_days' => 0, 'loss_rate_percent' => 0.628491, 'sort_order' => 10, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_001_030', 'bucket_name' => 'Overdue [>0DPD>30DPD]', 'min_days' => 1, 'max_days' => 30, 'loss_rate_percent' => 1.462967, 'sort_order' => 20, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_031_060', 'bucket_name' => 'Overdue [>30DPD>60DPD]', 'min_days' => 31, 'max_days' => 60, 'loss_rate_percent' => 3.405415, 'sort_order' => 30, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_061_090', 'bucket_name' => 'Overdue [>60DPD>90DPD]', 'min_days' => 61, 'max_days' => 90, 'loss_rate_percent' => 7.926937, 'sort_order' => 40, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_091_180', 'bucket_name' => 'Overdue [>90DPD>180DPD]', 'min_days' => 91, 'max_days' => 180, 'loss_rate_percent' => 18.451888, 'sort_order' => 50, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_181_360', 'bucket_name' => 'Overdue [>180DPD>360DPD]', 'min_days' => 181, 'max_days' => 360, 'loss_rate_percent' => 42.951290, 'sort_order' => 60, 'is_active' => 1],
            ['company_id' => $companyId, 'bucket_code' => 'OD_361_PLUS', 'bucket_name' => 'Overdue [>360DPD]', 'min_days' => 361, 'max_days' => 0, 'loss_rate_percent' => 100.000000, 'sort_order' => 70, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'ecl_report_categories', $companyId) === 0) {
        app_seed_insert_rows($pdo, 'ecl_report_categories', [
            ['company_id' => $companyId, 'category_code' => 'BANK_LOCAL', 'category_name' => 'Bank Accounts and Deposits - Local', 'source_type' => 'bank_accounts', 'loss_model' => 'country_12m', 'country_code' => '', 'account_codes' => '', 'financial_statement_group_code' => '', 'manual_amount' => 0, 'lgd_percent' => 45, 'rate_override_percent' => 0, 'sort_order' => 10, 'is_active' => 1],
            ['company_id' => $companyId, 'category_code' => 'BANK_FOREIGN', 'category_name' => 'Bank Accounts and Deposits - Foreign', 'source_type' => 'account_balances', 'loss_model' => 'country_12m', 'country_code' => '', 'account_codes' => '', 'financial_statement_group_code' => '', 'manual_amount' => 0, 'lgd_percent' => 45, 'rate_override_percent' => 0, 'sort_order' => 20, 'is_active' => 1],
            ['company_id' => $companyId, 'category_code' => 'AR', 'category_name' => "Account's Receivables", 'source_type' => 'sales_receivables', 'loss_model' => 'receivable_aging', 'country_code' => '', 'account_codes' => '', 'financial_statement_group_code' => '', 'manual_amount' => 0, 'lgd_percent' => 100, 'rate_override_percent' => 0, 'sort_order' => 30, 'is_active' => 1],
            ['company_id' => $companyId, 'category_code' => 'RELATED_PARTIES', 'category_name' => 'Due From Related parties', 'source_type' => 'account_balances', 'loss_model' => 'country_12m', 'country_code' => '', 'account_codes' => '', 'financial_statement_group_code' => '', 'manual_amount' => 0, 'lgd_percent' => 45, 'rate_override_percent' => 0, 'sort_order' => 40, 'is_active' => 1],
            ['company_id' => $companyId, 'category_code' => 'OTHER_DEBIT', 'category_name' => 'Other Debt Balances', 'source_type' => 'account_balances', 'loss_model' => 'country_12m', 'country_code' => '', 'account_codes' => '', 'financial_statement_group_code' => '', 'manual_amount' => 0, 'lgd_percent' => 45, 'rate_override_percent' => 0, 'sort_order' => 50, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'bank_accounts', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'bank_accounts', [
            ['company_id' => $companyId, 'account_name' => 'Main Operating Account', 'bank_name' => 'Al Rajhi Bank', 'gl_account_code' => '1112', 'iban' => 'SA0000000000000000000001', 'account_number' => '100001', 'opening_balance' => 250000, 'current_balance' => 250000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_name' => 'Payroll Account', 'bank_name' => 'SNB', 'gl_account_code' => '1113', 'iban' => 'SA0000000000000000000002', 'account_number' => '100002', 'opening_balance' => 120000, 'current_balance' => 120000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_name' => 'Petty Cash Vault', 'bank_name' => 'Cash', 'gl_account_code' => '1111', 'iban' => '', 'account_number' => 'CASH01', 'opening_balance' => 15000, 'current_balance' => 15000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_name' => 'Projects Account', 'bank_name' => 'Riyad Bank', 'gl_account_code' => '1112', 'iban' => 'SA0000000000000000000003', 'account_number' => '100003', 'opening_balance' => 80000, 'current_balance' => 80000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_name' => 'Savings Reserve', 'bank_name' => 'Alinma', 'gl_account_code' => '1112', 'iban' => 'SA0000000000000000000004', 'account_number' => '100004', 'opening_balance' => 50000, 'current_balance' => 50000, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'cash_accounts', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'cash_accounts', [
            ['company_id' => $companyId, 'cash_name' => 'Petty Cash Vault', 'gl_account_code' => '1111', 'custodian_name' => 'Admin Office', 'location_name' => 'Main Campus', 'opening_balance' => 15000, 'current_balance' => 15000, 'is_active' => 1],
            ['company_id' => $companyId, 'cash_name' => 'Reception Cash Box', 'gl_account_code' => '1111', 'custodian_name' => 'Reception', 'location_name' => 'Front Desk', 'opening_balance' => 4500, 'current_balance' => 4500, 'is_active' => 1],
            ['company_id' => $companyId, 'cash_name' => 'Activities Cash Box', 'gl_account_code' => '1111', 'custodian_name' => 'Activities Coordinator', 'location_name' => 'Activities Office', 'opening_balance' => 3000, 'current_balance' => 3000, 'is_active' => 1],
            ['company_id' => $companyId, 'cash_name' => 'Transport Cash Box', 'gl_account_code' => '1111', 'custodian_name' => 'Transport Supervisor', 'location_name' => 'Bus Yard', 'opening_balance' => 2500, 'current_balance' => 2500, 'is_active' => 1],
            ['company_id' => $companyId, 'cash_name' => 'Cafeteria Cash Box', 'gl_account_code' => '1111', 'custodian_name' => 'Cafeteria Manager', 'location_name' => 'Cafeteria', 'opening_balance' => 6000, 'current_balance' => 6000, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'cost_centers', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'cost_centers', [
            ['company_id' => $companyId, 'center_code' => 'CC-ADM', 'center_name' => 'Administration', 'is_active' => 1],
            ['company_id' => $companyId, 'center_code' => 'CC-PRI', 'center_name' => 'Primary School', 'is_active' => 1],
            ['company_id' => $companyId, 'center_code' => 'CC-SEC', 'center_name' => 'Secondary School', 'is_active' => 1],
            ['company_id' => $companyId, 'center_code' => 'CC-TRN', 'center_name' => 'Transport Department', 'is_active' => 1],
            ['company_id' => $companyId, 'center_code' => 'CC-ACT', 'center_name' => 'Activities and Clubs', 'is_active' => 1],
        ]);
    }

    if ($isExcellenceTarget) {
        app_seed_upsert_chart_accounts($pdo, [
            ['company_id' => $companyId, 'account_code' => '1000', 'account_name' => 'الأصول', 'account_type' => 'asset', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1100', 'account_name' => 'الأصول المتداولة', 'account_type' => 'asset', 'parent_code' => '1000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1110', 'account_name' => 'النقدية وما في حكمها', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1111', 'account_name' => 'الصندوق الرئيسي', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 15000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1112', 'account_name' => 'البنك الرئيسي', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 250000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1113', 'account_name' => 'بنك الرواتب والمصروفات', 'account_type' => 'asset', 'parent_code' => '1110', 'opening_balance' => 120000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1120', 'account_name' => 'العملاء وأوراق القبض', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1121', 'account_name' => 'العملاء', 'account_type' => 'asset', 'parent_code' => '1120', 'opening_balance' => 27500, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1130', 'account_name' => 'المخزون', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1131', 'account_name' => 'مخزون مستلزمات طبية', 'account_type' => 'asset', 'parent_code' => '1130', 'opening_balance' => 38000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1132', 'account_name' => 'مخزون قطع غيار وصيانة أجهزة', 'account_type' => 'asset', 'parent_code' => '1130', 'opening_balance' => 21000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1140', 'account_name' => 'ضريبة القيمة المضافة - مدخلات', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1150', 'account_name' => 'مصروفات مقدمة', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 5000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1160', 'account_name' => 'تأمينات ودفعات مقدمة للغير', 'account_type' => 'asset', 'parent_code' => '1100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1200', 'account_name' => 'الأصول غير المتداولة', 'account_type' => 'asset', 'parent_code' => '1000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1210', 'account_name' => 'أصول ثابتة', 'account_type' => 'asset', 'parent_code' => '1200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1211', 'account_name' => 'أجهزة ومعدات طبية', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 1800000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1212', 'account_name' => 'أثاث وتجهيزات', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 360000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1213', 'account_name' => 'أجهزة حاسب وبرامج', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 240000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1214', 'account_name' => 'سيارات ومركبات', 'account_type' => 'asset', 'parent_code' => '1210', 'opening_balance' => 820000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '1220', 'account_name' => 'مجمع الإهلاك', 'account_type' => 'asset', 'parent_code' => '1200', 'opening_balance' => -120000, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '2000', 'account_name' => 'الالتزامات', 'account_type' => 'liability', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2100', 'account_name' => 'الالتزامات المتداولة', 'account_type' => 'liability', 'parent_code' => '2000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2110', 'account_name' => 'الموردون وأوراق الدفع', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2111', 'account_name' => 'الموردون', 'account_type' => 'liability', 'parent_code' => '2110', 'opening_balance' => 22000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2120', 'account_name' => 'ضريبة القيمة المضافة - مخرجات', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2130', 'account_name' => 'مصروفات مستحقة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 14000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2140', 'account_name' => 'إيرادات مقدمة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 30000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2150', 'account_name' => 'تأمينات اجتماعية مستحقة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2155', 'account_name' => 'ضريبة كسب العمل مستحقة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2160', 'account_name' => 'ضريبة الأرباح التجارية والصناعية مستحقة - خصم وإضافة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2170', 'account_name' => 'ضرائب ورسوم أخرى مستحقة', 'account_type' => 'liability', 'parent_code' => '2100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2200', 'account_name' => 'التزامات طويلة الأجل', 'account_type' => 'liability', 'parent_code' => '2000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '2210', 'account_name' => 'قروض والتزامات تمويلية طويلة الأجل', 'account_type' => 'liability', 'parent_code' => '2200', 'opening_balance' => 180000, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '3000', 'account_name' => 'حقوق الملكية', 'account_type' => 'equity', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3100', 'account_name' => 'رأس المال', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 2200000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3200', 'account_name' => 'أرباح مرحلة', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 340000, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '3300', 'account_name' => 'أرباح/خسائر العام', 'account_type' => 'equity', 'parent_code' => '3000', 'opening_balance' => 0, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '4000', 'account_name' => 'الإيرادات', 'account_type' => 'revenue', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4100', 'account_name' => 'إيرادات التشغيل', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4110', 'account_name' => 'إيرادات تأجير أجهزة طبية', 'account_type' => 'revenue', 'parent_code' => '4100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4120', 'account_name' => 'إيرادات خدمات تشغيل ومتابعة', 'account_type' => 'revenue', 'parent_code' => '4100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4130', 'account_name' => 'إيرادات مستلزمات طبية', 'account_type' => 'revenue', 'parent_code' => '4100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4140', 'account_name' => 'إيرادات خدمات وفواتير أخرى', 'account_type' => 'revenue', 'parent_code' => '4100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4200', 'account_name' => 'إيرادات أخرى', 'account_type' => 'revenue', 'parent_code' => '4000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4210', 'account_name' => 'إيرادات أنشطة وخدمات إضافية', 'account_type' => 'revenue', 'parent_code' => '4200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4220', 'account_name' => 'غرامات تأخير وفروق تسوية', 'account_type' => 'revenue', 'parent_code' => '4200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '4230', 'account_name' => 'إيرادات تأجير أجهزة طبية - EXcellence', 'account_type' => 'revenue', 'parent_code' => '4100', 'opening_balance' => 0, 'is_active' => 1],

            ['company_id' => $companyId, 'account_code' => '5000', 'account_name' => 'المصروفات', 'account_type' => 'expense', 'parent_code' => '', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5100', 'account_name' => 'تكلفة الإيراد', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5110', 'account_name' => 'تكاليف عمليات المستشفيات', 'account_type' => 'expense', 'parent_code' => '5100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5120', 'account_name' => 'مستلزمات عمليات', 'account_type' => 'expense', 'parent_code' => '5100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5200', 'account_name' => 'الرواتب والأجور', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5210', 'account_name' => 'رواتب إدارية', 'account_type' => 'expense', 'parent_code' => '5200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5220', 'account_name' => 'رواتب فنيين تشغيل', 'account_type' => 'expense', 'parent_code' => '5200', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5300', 'account_name' => 'مصروفات عمومية وإدارية', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5310', 'account_name' => 'إيجارات', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5320', 'account_name' => 'كهرباء ومياه ومرافق', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5330', 'account_name' => 'صيانة وإصلاح', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5340', 'account_name' => 'انتقالات ومواصلات', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5350', 'account_name' => 'أدوات مكتبية ومطبوعات', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5360', 'account_name' => 'أتعاب مهنية واستشارات', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5400', 'account_name' => 'الإهلاك', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5410', 'account_name' => 'إهلاك أجهزة ومعدات طبية', 'account_type' => 'expense', 'parent_code' => '5400', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5420', 'account_name' => 'إهلاك وتجديد تجهيزات أخرى', 'account_type' => 'expense', 'parent_code' => '5400', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5500', 'account_name' => 'مصروفات تمويلية وبنكية', 'account_type' => 'expense', 'parent_code' => '5000', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5510', 'account_name' => 'أدوات ومهمات مكتبية', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5520', 'account_name' => 'اتصالات وإنترنت', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5530', 'account_name' => 'أتعاب مهنية واستشارات', 'account_type' => 'expense', 'parent_code' => '5300', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5540', 'account_name' => 'تكاليف عمليات المستشفيات - EXcellence', 'account_type' => 'expense', 'parent_code' => '5100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5550', 'account_name' => 'مصروفات تشغيل الأجهزة الطبية', 'account_type' => 'expense', 'parent_code' => '5100', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5600', 'account_name' => 'مصروف إهلاك عام', 'account_type' => 'expense', 'parent_code' => '5400', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'account_code' => '5700', 'account_name' => 'فوائد ومصاريف بنكية', 'account_type' => 'expense', 'parent_code' => '5500', 'opening_balance' => 0, 'is_active' => 1],
        ]);

        app_seed_insert_rows($pdo, 'cost_centers', [
            ['company_id' => $companyId, 'center_code' => 'CC-MED', 'center_name' => 'عمليات تأجير الأجهزة الطبية', 'is_active' => 1],
        ]);

        app_seed_tax_rate_if_missing($pdo, $companyId, 'قيمة مضافة مبيعات 14%', 14, 'sales', '2120');
        app_seed_tax_rate_if_missing($pdo, $companyId, 'قيمة مضافة مشتريات 14%', 14, 'purchase', '1140');
        app_seed_tax_rate_if_missing($pdo, $companyId, 'خصم وإضافة - توريدات 1%', 1, 'withholding', '2160');
        app_seed_tax_rate_if_missing($pdo, $companyId, 'خصم وإضافة - خدمات 3%', 3, 'withholding', '2160');
        app_seed_tax_rate_if_missing($pdo, $companyId, 'خصم وإضافة - مقاولات 5%', 5, 'withholding', '2160');

        app_seed_insert_rows($pdo, 'contacts', [
            ['company_id' => $companyId, 'contact_type' => 'vendor', 'contact_code' => 'VEND-HSP', 'contact_name' => 'مستشفى تجريبي', 'tax_number' => '123456789', 'withholding_tax_name' => 'خصم وإضافة - خدمات 3%', 'withholding_tax_rate_percent' => 3, 'phone' => '01000000001', 'email' => 'hospital@example.com', 'address' => 'القاهرة', 'opening_balance' => 0, 'is_active' => 1],
            ['company_id' => $companyId, 'contact_type' => 'vendor', 'contact_code' => 'VEND-SUP', 'contact_name' => 'مورد مستلزمات طبية', 'tax_number' => '987654321', 'withholding_tax_name' => 'خصم وإضافة - توريدات 1%', 'withholding_tax_rate_percent' => 1, 'phone' => '01000000002', 'email' => 'supplier@example.com', 'address' => 'الجيزة', 'opening_balance' => 0, 'is_active' => 1],
        ]);
        $pdo->prepare(
            "UPDATE contacts
             SET withholding_tax_name = 'خصم وإضافة - توريدات 1%',
                 withholding_tax_rate_percent = 1
             WHERE company_id = :company_id
               AND contact_type IN ('vendor', 'both')
               AND (withholding_tax_name = '' OR withholding_tax_name IS NULL)"
        )->execute(['company_id' => $companyId]);

        if (app_seed_table_count($pdo, 'excellence_hospitals', $companyId) < 3) {
            app_seed_insert_rows($pdo, 'excellence_hospitals', [
                ['company_id' => $companyId, 'hospital_code' => 'HSP-001', 'hospital_name' => 'مستشفى القاهرة التخصصي', 'tax_number' => '300123456700003', 'phone' => '+201000100100', 'email' => 'ops@cairocare.example', 'address' => 'القاهرة', 'is_active' => 1, 'notes' => 'مستشفى تجريبية لشركة EXcellence.'],
                ['company_id' => $companyId, 'hospital_code' => 'HSP-002', 'hospital_name' => 'مركز الجيزة الطبي', 'tax_number' => '300123456700004', 'phone' => '+201000100200', 'email' => 'ops@gizamc.example', 'address' => 'الجيزة', 'is_active' => 1, 'notes' => 'مركز طبي تجريبي.'],
                ['company_id' => $companyId, 'hospital_code' => 'HSP-003', 'hospital_name' => 'مستشفى الإسكندرية للجراحات', 'tax_number' => '300123456700005', 'phone' => '+201000100300', 'email' => 'ops@alexsurgery.example', 'address' => 'الإسكندرية', 'is_active' => 1, 'notes' => 'مستشفى تجريبية.'],
            ]);
        }

        if (app_seed_table_count($pdo, 'excellence_devices', $companyId) < 3) {
            app_seed_insert_rows($pdo, 'excellence_devices', [
                ['company_id' => $companyId, 'device_code' => 'DEV-001', 'device_name' => 'برج منظار جراحي', 'serial_number' => 'LP-1001', 'category' => 'laparoscopy', 'default_rental_rate' => 6500, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'is_active' => 1, 'notes' => 'جهاز تأجير تجريبي.'],
                ['company_id' => $companyId, 'device_code' => 'DEV-002', 'device_name' => 'طقم مثقاب عظام', 'serial_number' => 'OR-2001', 'category' => 'orthopedic', 'default_rental_rate' => 4200, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'is_active' => 1, 'notes' => 'جهاز تأجير تجريبي.'],
                ['company_id' => $companyId, 'device_code' => 'DEV-003', 'device_name' => 'جهاز مراقبة مريض', 'serial_number' => 'PM-3001', 'category' => 'monitoring', 'default_rental_rate' => 1800, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'is_active' => 1, 'notes' => 'جهاز تأجير تجريبي.'],
            ]);
        }

        if (app_seed_table_count($pdo, 'excellence_operations', $companyId) < 5) {
            app_seed_insert_rows($pdo, 'excellence_operations', [
                ['company_id' => $companyId, 'operation_number' => 'EXOP-000001', 'operation_date' => $today, 'hospital_code' => 'HSP-001', 'patient_name' => 'مريض 1', 'patient_file_no' => 'MRN-1001', 'procedure_name' => 'عملية منظار', 'doctor_name' => 'د. سالم', 'device_code' => 'DEV-001', 'quantity' => 1, 'rental_rate' => 6500, 'revenue_amount' => 6500, 'hospital_expense_amount' => 1800, 'net_operation_income' => 4700, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'cost_center_code' => 'CC-MED', 'status' => 'confirmed', 'notes' => 'عملية ديمو مؤكدة.'],
                ['company_id' => $companyId, 'operation_number' => 'EXOP-000002', 'operation_date' => $today, 'hospital_code' => 'HSP-002', 'patient_name' => 'مريض 2', 'patient_file_no' => 'MRN-1002', 'procedure_name' => 'جراحة عظام', 'doctor_name' => 'د. ندى', 'device_code' => 'DEV-002', 'quantity' => 1, 'rental_rate' => 4200, 'revenue_amount' => 4200, 'hospital_expense_amount' => 1200, 'net_operation_income' => 3000, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'cost_center_code' => 'CC-MED', 'status' => 'confirmed', 'notes' => 'عملية ديمو مؤكدة.'],
                ['company_id' => $companyId, 'operation_number' => 'EXOP-000003', 'operation_date' => $today, 'hospital_code' => 'HSP-003', 'patient_name' => 'مريض 3', 'patient_file_no' => 'MRN-1003', 'procedure_name' => 'دعم مراقبة', 'doctor_name' => 'د. عمر', 'device_code' => 'DEV-003', 'quantity' => 2, 'rental_rate' => 1800, 'revenue_amount' => 3600, 'hospital_expense_amount' => 700, 'net_operation_income' => 2900, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'cost_center_code' => 'CC-MED', 'status' => 'draft', 'notes' => 'عملية ديمو مسودة.'],
                ['company_id' => $companyId, 'operation_number' => 'EXOP-000004', 'operation_date' => $today, 'hospital_code' => 'HSP-001', 'patient_name' => 'مريض 4', 'patient_file_no' => 'MRN-1004', 'procedure_name' => 'منظار مرارة', 'doctor_name' => 'د. هاني', 'device_code' => 'DEV-001', 'quantity' => 1, 'rental_rate' => 7200, 'revenue_amount' => 7200, 'hospital_expense_amount' => 2100, 'net_operation_income' => 5100, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'cost_center_code' => 'CC-MED', 'status' => 'confirmed', 'notes' => 'عملية ديمو مؤكدة.'],
                ['company_id' => $companyId, 'operation_number' => 'EXOP-000005', 'operation_date' => $today, 'hospital_code' => 'HSP-002', 'patient_name' => 'مريض 5', 'patient_file_no' => 'MRN-1005', 'procedure_name' => 'تثبيت كسر', 'doctor_name' => 'د. منى', 'device_code' => 'DEV-002', 'quantity' => 1, 'rental_rate' => 5000, 'revenue_amount' => 5000, 'hospital_expense_amount' => 1500, 'net_operation_income' => 3500, 'revenue_account_code' => '4230', 'hospital_expense_account_code' => '5540', 'cost_center_code' => 'CC-MED', 'status' => 'confirmed', 'notes' => 'عملية ديمو مؤكدة.'],
            ]);
        }
    }

    if (app_seed_table_count($pdo, 'student_stages', $companyId) < 4) {
        app_seed_insert_rows($pdo, 'student_stages', [
            ['company_id' => $companyId, 'stage_code' => 'KG', 'stage_name' => 'Kindergarten', 'sort_order' => 10, 'is_active' => 1, 'notes' => 'Saudi KG stage.'],
            ['company_id' => $companyId, 'stage_code' => 'PRIMARY', 'stage_name' => 'Primary', 'sort_order' => 20, 'is_active' => 1, 'notes' => 'Grades 1 to 6.'],
            ['company_id' => $companyId, 'stage_code' => 'INTERMEDIATE', 'stage_name' => 'Intermediate', 'sort_order' => 30, 'is_active' => 1, 'notes' => 'Grades 7 to 9.'],
            ['company_id' => $companyId, 'stage_code' => 'SECONDARY', 'stage_name' => 'Secondary', 'sort_order' => 40, 'is_active' => 1, 'notes' => 'Grades 10 to 12.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_grades', $companyId) < 15) {
        app_seed_insert_rows($pdo, 'student_grades', [
            ['company_id' => $companyId, 'grade_code' => 'KG1', 'grade_name' => 'KG 1', 'stage_code' => 'KG', 'sort_order' => 1, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'KG2', 'grade_name' => 'KG 2', 'stage_code' => 'KG', 'sort_order' => 2, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'KG3', 'grade_name' => 'KG 3', 'stage_code' => 'KG', 'sort_order' => 3, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G1', 'grade_name' => 'Grade 1', 'stage_code' => 'PRIMARY', 'sort_order' => 11, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G2', 'grade_name' => 'Grade 2', 'stage_code' => 'PRIMARY', 'sort_order' => 12, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G3', 'grade_name' => 'Grade 3', 'stage_code' => 'PRIMARY', 'sort_order' => 13, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G4', 'grade_name' => 'Grade 4', 'stage_code' => 'PRIMARY', 'sort_order' => 14, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G5', 'grade_name' => 'Grade 5', 'stage_code' => 'PRIMARY', 'sort_order' => 15, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G6', 'grade_name' => 'Grade 6', 'stage_code' => 'PRIMARY', 'sort_order' => 16, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G7', 'grade_name' => 'Grade 7', 'stage_code' => 'INTERMEDIATE', 'sort_order' => 21, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G8', 'grade_name' => 'Grade 8', 'stage_code' => 'INTERMEDIATE', 'sort_order' => 22, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G9', 'grade_name' => 'Grade 9', 'stage_code' => 'INTERMEDIATE', 'sort_order' => 23, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G10', 'grade_name' => 'Grade 10', 'stage_code' => 'SECONDARY', 'sort_order' => 31, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G11', 'grade_name' => 'Grade 11', 'stage_code' => 'SECONDARY', 'sort_order' => 32, 'is_active' => 1],
            ['company_id' => $companyId, 'grade_code' => 'G12', 'grade_name' => 'Grade 12', 'stage_code' => 'SECONDARY', 'sort_order' => 33, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_classes', $companyId) < 8) {
        app_seed_insert_rows($pdo, 'student_classes', [
            ['company_id' => $companyId, 'class_code' => 'KG1-A', 'class_name' => 'KG1 A', 'academic_year' => (string) $year, 'stage_code' => 'KG', 'grade_code' => 'KG1', 'capacity' => 24, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'KG2-A', 'class_name' => 'KG2 A', 'academic_year' => (string) $year, 'stage_code' => 'KG', 'grade_code' => 'KG2', 'capacity' => 24, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G1-A', 'class_name' => 'Grade 1 A', 'academic_year' => (string) $year, 'stage_code' => 'PRIMARY', 'grade_code' => 'G1', 'capacity' => 28, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G2-A', 'class_name' => 'Grade 2 A', 'academic_year' => (string) $year, 'stage_code' => 'PRIMARY', 'grade_code' => 'G2', 'capacity' => 28, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G7-A', 'class_name' => 'Grade 7 A', 'academic_year' => (string) $year, 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G7', 'capacity' => 30, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G8-A', 'class_name' => 'Grade 8 A', 'academic_year' => (string) $year, 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G8', 'capacity' => 30, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G10-A', 'class_name' => 'Grade 10 A', 'academic_year' => (string) $year, 'stage_code' => 'SECONDARY', 'grade_code' => 'G10', 'capacity' => 32, 'is_active' => 1],
            ['company_id' => $companyId, 'class_code' => 'G12-A', 'class_name' => 'Grade 12 A', 'academic_year' => (string) $year, 'stage_code' => 'SECONDARY', 'grade_code' => 'G12', 'capacity' => 32, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_revenue_types', $companyId) < 8) {
        app_seed_insert_rows($pdo, 'student_revenue_types', [
            ['company_id' => $companyId, 'revenue_type_code' => 'TUITION', 'revenue_type_name' => 'Tuition Fees', 'revenue_category' => 'tuition', 'default_amount' => 12500, 'is_taxable' => 0, 'tax_name' => '', 'revenue_account_code' => '4100', 'cost_center_code' => 'CC-PRI', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'REG', 'revenue_type_name' => 'Registration Fees', 'revenue_category' => 'registration', 'default_amount' => 1200, 'is_taxable' => 0, 'tax_name' => '', 'revenue_account_code' => '4110', 'cost_center_code' => 'CC-ADM', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'TRANSPORT', 'revenue_type_name' => 'Transport Fees', 'revenue_category' => 'transport', 'default_amount' => 3200, 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'revenue_account_code' => '4120', 'cost_center_code' => 'CC-TRN', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'BOOKS', 'revenue_type_name' => 'Books and Worksheets', 'revenue_category' => 'books', 'default_amount' => 850, 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'revenue_account_code' => '4130', 'cost_center_code' => 'CC-PRI', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'UNIFORM', 'revenue_type_name' => 'Uniform Fees', 'revenue_category' => 'uniform', 'default_amount' => 620, 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'revenue_account_code' => '4140', 'cost_center_code' => 'CC-ADM', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'ACT', 'revenue_type_name' => 'Activities Fees', 'revenue_category' => 'activities', 'default_amount' => 450, 'is_taxable' => 0, 'tax_name' => '', 'revenue_account_code' => '4210', 'cost_center_code' => 'CC-ACT', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'EXAM', 'revenue_type_name' => 'Exam Fees', 'revenue_category' => 'exam', 'default_amount' => 300, 'is_taxable' => 0, 'tax_name' => '', 'revenue_account_code' => '4200', 'cost_center_code' => 'CC-SEC', 'is_active' => 1],
            ['company_id' => $companyId, 'revenue_type_code' => 'LATE', 'revenue_type_name' => 'Late Payment Fees', 'revenue_category' => 'other', 'default_amount' => 150, 'is_taxable' => 0, 'tax_name' => '', 'revenue_account_code' => '4220', 'cost_center_code' => 'CC-ADM', 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'students', $companyId) < 8) {
        app_seed_insert_rows($pdo, 'students', [
            ['company_id' => $companyId, 'student_number' => 'STD001', 'student_name' => 'Faisal Ahmed Ali', 'national_id' => '1100000001', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'PRIMARY', 'grade_code' => 'G1', 'class_code' => 'G1-A', 'enrollment_date' => $year . '-08-20', 'status' => 'active', 'notes' => 'Demo student.'],
            ['company_id' => $companyId, 'student_number' => 'STD002', 'student_name' => 'Noura Ahmed Ali', 'national_id' => '1100000002', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'KG', 'grade_code' => 'KG2', 'class_code' => 'KG2-A', 'enrollment_date' => $year . '-08-20', 'status' => 'active', 'notes' => 'Sibling account.'],
            ['company_id' => $companyId, 'student_number' => 'STD003', 'student_name' => 'Sara Omar Saleh', 'national_id' => '1100000003', 'guardian_contact_code' => 'CUST002', 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G7', 'class_code' => 'G7-A', 'enrollment_date' => $year . '-08-21', 'status' => 'active', 'notes' => 'Demo student.'],
            ['company_id' => $companyId, 'student_number' => 'STD004', 'student_name' => 'Yara Omar Saleh', 'national_id' => '1100000004', 'guardian_contact_code' => 'CUST002', 'stage_code' => 'PRIMARY', 'grade_code' => 'G2', 'class_code' => 'G2-A', 'enrollment_date' => $year . '-08-21', 'status' => 'active', 'notes' => 'Demo student.'],
            ['company_id' => $companyId, 'student_number' => 'STD005', 'student_name' => 'Khaled Sami', 'national_id' => '1100000005', 'guardian_contact_code' => 'CUST003', 'stage_code' => 'SECONDARY', 'grade_code' => 'G10', 'class_code' => 'G10-A', 'enrollment_date' => $year . '-08-22', 'status' => 'active', 'notes' => 'Demo student.'],
            ['company_id' => $companyId, 'student_number' => 'STD006', 'student_name' => 'Lina Khaled Sami', 'national_id' => '1100000006', 'guardian_contact_code' => 'CUST003', 'stage_code' => 'KG', 'grade_code' => 'KG1', 'class_code' => 'KG1-A', 'enrollment_date' => $year . '-08-22', 'status' => 'active', 'notes' => 'Demo student.'],
            ['company_id' => $companyId, 'student_number' => 'STD007', 'student_name' => 'Rakan Fahad', 'national_id' => '1100000007', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'SECONDARY', 'grade_code' => 'G12', 'class_code' => 'G12-A', 'enrollment_date' => $year . '-08-22', 'status' => 'active', 'notes' => 'Graduation year sample.'],
            ['company_id' => $companyId, 'student_number' => 'STD008', 'student_name' => 'Maha Saad', 'national_id' => '1100000008', 'guardian_contact_code' => 'CUST002', 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G8', 'class_code' => 'G8-A', 'enrollment_date' => $year . '-08-22', 'status' => 'active', 'notes' => 'Demo student.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_fee_plans', $companyId) < 6) {
        app_seed_insert_rows($pdo, 'student_fee_plans', [
            ['company_id' => $companyId, 'fee_plan_code' => 'FEE-KG', 'fee_plan_name' => 'KG Tuition Plan', 'academic_year' => (string) $year, 'stage_code' => 'KG', 'grade_code' => 'KG2', 'revenue_type_code' => 'TUITION', 'amount' => 9500, 'is_taxable' => 0, 'tax_name' => '', 'installments_count' => 3, 'due_day' => 5, 'is_active' => 1],
            ['company_id' => $companyId, 'fee_plan_code' => 'FEE-G1', 'fee_plan_name' => 'Grade 1 Tuition Plan', 'academic_year' => (string) $year, 'stage_code' => 'PRIMARY', 'grade_code' => 'G1', 'revenue_type_code' => 'TUITION', 'amount' => 12500, 'is_taxable' => 0, 'tax_name' => '', 'installments_count' => 3, 'due_day' => 5, 'is_active' => 1],
            ['company_id' => $companyId, 'fee_plan_code' => 'FEE-G7', 'fee_plan_name' => 'Grade 7 Tuition Plan', 'academic_year' => (string) $year, 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G7', 'revenue_type_code' => 'TUITION', 'amount' => 14800, 'is_taxable' => 0, 'tax_name' => '', 'installments_count' => 3, 'due_day' => 5, 'is_active' => 1],
            ['company_id' => $companyId, 'fee_plan_code' => 'FEE-G12', 'fee_plan_name' => 'Grade 12 Tuition Plan', 'academic_year' => (string) $year, 'stage_code' => 'SECONDARY', 'grade_code' => 'G12', 'revenue_type_code' => 'TUITION', 'amount' => 18500, 'is_taxable' => 0, 'tax_name' => '', 'installments_count' => 2, 'due_day' => 5, 'is_active' => 1],
            ['company_id' => $companyId, 'fee_plan_code' => 'TRN-ALL', 'fee_plan_name' => 'Transport Annual Plan', 'academic_year' => (string) $year, 'stage_code' => 'PRIMARY', 'grade_code' => 'G1', 'revenue_type_code' => 'TRANSPORT', 'amount' => 3200, 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'installments_count' => 1, 'due_day' => 5, 'is_active' => 1],
            ['company_id' => $companyId, 'fee_plan_code' => 'BOOKS-PRI', 'fee_plan_name' => 'Primary Books Plan', 'academic_year' => (string) $year, 'stage_code' => 'PRIMARY', 'grade_code' => 'G2', 'revenue_type_code' => 'BOOKS', 'amount' => 850, 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'installments_count' => 1, 'due_day' => 5, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_invoices', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'student_invoices', [
            ['company_id' => $companyId, 'student_invoice_number' => 'STINV-0001', 'invoice_date' => $today, 'academic_year' => (string) $year, 'student_number' => 'STD001', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'PRIMARY', 'grade_code' => 'G1', 'class_code' => 'G1-A', 'revenue_type_code' => 'TUITION', 'description' => 'Grade 1 tuition fees', 'revenue_account_code' => '4100', 'cost_center_code' => 'CC-PRI', 'is_taxable' => 0, 'tax_name' => '', 'tax_rate_percent' => 0, 'subtotal' => 12500, 'discount_total' => 500, 'tax_total' => 0, 'grand_total' => 12000, 'inline_paid_amount' => 2000, 'paid_amount' => 2000, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'status' => 'partially_paid', 'notes' => 'First installment collected.'],
            ['company_id' => $companyId, 'student_invoice_number' => 'STINV-0002', 'invoice_date' => $today, 'academic_year' => (string) $year, 'student_number' => 'STD002', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'KG', 'grade_code' => 'KG2', 'class_code' => 'KG2-A', 'revenue_type_code' => 'REG', 'description' => 'Registration fees', 'revenue_account_code' => '4110', 'cost_center_code' => 'CC-ADM', 'is_taxable' => 0, 'tax_name' => '', 'tax_rate_percent' => 0, 'subtotal' => 1200, 'discount_total' => 0, 'tax_total' => 0, 'grand_total' => 1200, 'inline_paid_amount' => 1200, 'paid_amount' => 1200, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'status' => 'paid', 'notes' => 'Paid at reception.'],
            ['company_id' => $companyId, 'student_invoice_number' => 'STINV-0003', 'invoice_date' => $today, 'academic_year' => (string) $year, 'student_number' => 'STD003', 'guardian_contact_code' => 'CUST002', 'stage_code' => 'INTERMEDIATE', 'grade_code' => 'G7', 'class_code' => 'G7-A', 'revenue_type_code' => 'TRANSPORT', 'description' => 'Transport fees', 'revenue_account_code' => '4120', 'cost_center_code' => 'CC-TRN', 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'tax_rate_percent' => 15, 'subtotal' => 3200, 'discount_total' => 0, 'tax_total' => 480, 'grand_total' => 3680, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'issued', 'notes' => 'Transport route invoice.'],
            ['company_id' => $companyId, 'student_invoice_number' => 'STINV-0004', 'invoice_date' => $today, 'academic_year' => (string) $year, 'student_number' => 'STD004', 'guardian_contact_code' => 'CUST002', 'stage_code' => 'PRIMARY', 'grade_code' => 'G2', 'class_code' => 'G2-A', 'revenue_type_code' => 'BOOKS', 'description' => 'Books set', 'revenue_account_code' => '4130', 'cost_center_code' => 'CC-PRI', 'is_taxable' => 1, 'tax_name' => 'VAT Sales 15%', 'tax_rate_percent' => 15, 'subtotal' => 850, 'discount_total' => 0, 'tax_total' => 127.5, 'grand_total' => 977.5, 'inline_paid_amount' => 977.5, 'paid_amount' => 977.5, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'status' => 'paid', 'notes' => 'Books delivered.'],
            ['company_id' => $companyId, 'student_invoice_number' => 'STINV-0005', 'invoice_date' => $today, 'academic_year' => (string) $year, 'student_number' => 'STD007', 'guardian_contact_code' => 'CUST001', 'stage_code' => 'SECONDARY', 'grade_code' => 'G12', 'class_code' => 'G12-A', 'revenue_type_code' => 'EXAM', 'description' => 'Final exam fees', 'revenue_account_code' => '4200', 'cost_center_code' => 'CC-SEC', 'is_taxable' => 0, 'tax_name' => '', 'tax_rate_percent' => 0, 'subtotal' => 300, 'discount_total' => 0, 'tax_total' => 0, 'grand_total' => 300, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'issued', 'notes' => 'Due before exams.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'student_receipts', $companyId) < 3) {
        app_seed_insert_rows($pdo, 'student_receipts', [
            ['company_id' => $companyId, 'receipt_number' => 'STRC-0001', 'receipt_date' => $today, 'student_invoice_id' => null, 'student_number' => 'STD003', 'guardian_contact_code' => 'CUST002', 'amount' => 1000, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'reference' => 'STINV-0003', 'notes' => 'Transport partial collection.'],
            ['company_id' => $companyId, 'receipt_number' => 'STRC-0002', 'receipt_date' => $today, 'student_invoice_id' => null, 'student_number' => 'STD007', 'guardian_contact_code' => 'CUST001', 'amount' => 300, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'reference' => 'STINV-0005', 'notes' => 'Exam fees collection.'],
            ['company_id' => $companyId, 'receipt_number' => 'STRC-0003', 'receipt_date' => $today, 'student_invoice_id' => null, 'student_number' => 'STD006', 'guardian_contact_code' => 'CUST003', 'amount' => 500, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'reference' => 'ADV-STD006', 'notes' => 'Advance receipt.'],
        ]);
        $pdo->prepare("UPDATE student_receipts r INNER JOIN student_invoices i ON i.company_id = r.company_id AND i.student_invoice_number = r.reference SET r.student_invoice_id = i.id WHERE r.company_id = :company_id AND (r.student_invoice_id IS NULL OR r.student_invoice_id = 0)")->execute(['company_id' => $companyId]);
    }

    if (app_seed_table_count($pdo, 'student_refunds', $companyId) < 2) {
        app_seed_insert_rows($pdo, 'student_refunds', [
            ['company_id' => $companyId, 'voucher_number' => 'STPV-0001', 'voucher_date' => $today, 'student_number' => 'STD006', 'guardian_contact_code' => 'CUST003', 'amount' => 150, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'offset_account_code' => '2140', 'reason' => 'Advance refund', 'notes' => 'Demo refund voucher.'],
            ['company_id' => $companyId, 'voucher_number' => 'STPV-0002', 'voucher_date' => $today, 'student_number' => 'STD001', 'guardian_contact_code' => 'CUST001', 'amount' => 200, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'offset_account_code' => '2140', 'reason' => 'Sibling discount refund', 'notes' => 'Demo payment voucher.'],
        ]);
    }
    if (app_seed_table_count($pdo, 'hr_departments', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_departments', [
            ['company_id' => $companyId, 'department_code' => 'HR-ADM', 'department_name' => 'Administration', 'manager_name' => 'Nadia Hassan', 'cost_center_code' => 'CC-ADM', 'is_active' => 1, 'notes' => 'School administration team.'],
            ['company_id' => $companyId, 'department_code' => 'HR-PRI', 'department_name' => 'Primary School', 'manager_name' => 'Omar Farouk', 'cost_center_code' => 'CC-PRI', 'is_active' => 1, 'notes' => 'Primary school teaching staff.'],
            ['company_id' => $companyId, 'department_code' => 'HR-SEC', 'department_name' => 'Secondary School', 'manager_name' => 'Mona Adel', 'cost_center_code' => 'CC-SEC', 'is_active' => 1, 'notes' => 'Secondary school teaching staff.'],
            ['company_id' => $companyId, 'department_code' => 'HR-FIN', 'department_name' => 'Finance', 'manager_name' => 'Mahdy Ahmed', 'cost_center_code' => 'CC-ADM', 'is_active' => 1, 'notes' => 'Finance and accounting.'],
            ['company_id' => $companyId, 'department_code' => 'HR-TRN', 'department_name' => 'Transportation', 'manager_name' => 'Hany Saad', 'cost_center_code' => 'CC-TRN', 'is_active' => 1, 'notes' => 'Bus and transport team.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_job_titles', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_job_titles', [
            ['company_id' => $companyId, 'job_code' => 'JOB-PRN', 'job_title' => 'Principal', 'department_code' => 'HR-ADM', 'grade_name' => 'G1', 'is_active' => 1, 'notes' => 'School leadership.'],
            ['company_id' => $companyId, 'job_code' => 'JOB-TCH', 'job_title' => 'Teacher', 'department_code' => 'HR-PRI', 'grade_name' => 'G3', 'is_active' => 1, 'notes' => 'Teaching staff.'],
            ['company_id' => $companyId, 'job_code' => 'JOB-SUP', 'job_title' => 'Supervisor', 'department_code' => 'HR-SEC', 'grade_name' => 'G2', 'is_active' => 1, 'notes' => 'Academic supervision.'],
            ['company_id' => $companyId, 'job_code' => 'JOB-ACC', 'job_title' => 'Accountant', 'department_code' => 'HR-FIN', 'grade_name' => 'G3', 'is_active' => 1, 'notes' => 'Finance staff.'],
            ['company_id' => $companyId, 'job_code' => 'JOB-DRV', 'job_title' => 'Bus Driver', 'department_code' => 'HR-TRN', 'grade_name' => 'G4', 'is_active' => 1, 'notes' => 'Transport operations.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_shifts', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_shifts', [
            ['company_id' => $companyId, 'shift_code' => 'SH-DAY', 'shift_name' => 'Regular School Day', 'start_time' => '08:00:00', 'end_time' => '16:00:00', 'working_hours' => 8, 'is_active' => 1, 'notes' => 'Default teaching and admin shift.'],
            ['company_id' => $companyId, 'shift_code' => 'SH-EARLY', 'shift_name' => 'Early Reception', 'start_time' => '07:00:00', 'end_time' => '15:00:00', 'working_hours' => 8, 'is_active' => 1, 'notes' => 'Reception and morning supervision.'],
            ['company_id' => $companyId, 'shift_code' => 'SH-BUS-AM', 'shift_name' => 'Morning Transport', 'start_time' => '05:30:00', 'end_time' => '13:30:00', 'working_hours' => 8, 'is_active' => 1, 'notes' => 'Bus drivers morning route.'],
            ['company_id' => $companyId, 'shift_code' => 'SH-BUS-PM', 'shift_name' => 'Afternoon Transport', 'start_time' => '10:30:00', 'end_time' => '18:30:00', 'working_hours' => 8, 'is_active' => 1, 'notes' => 'Bus drivers afternoon route.'],
            ['company_id' => $companyId, 'shift_code' => 'SH-PART', 'shift_name' => 'Part Time Academic', 'start_time' => '09:00:00', 'end_time' => '13:00:00', 'working_hours' => 4, 'is_active' => 1, 'notes' => 'Part time teachers and clubs.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_leave_types', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_leave_types', [
            ['company_id' => $companyId, 'leave_type_code' => 'LV-ANN', 'leave_type_name' => 'Annual Leave', 'annual_balance_days' => 21, 'is_paid' => 1, 'is_active' => 1, 'notes' => 'Regular paid annual leave.'],
            ['company_id' => $companyId, 'leave_type_code' => 'LV-SICK', 'leave_type_name' => 'Sick Leave', 'annual_balance_days' => 30, 'is_paid' => 1, 'is_active' => 1, 'notes' => 'Requires medical documentation.'],
            ['company_id' => $companyId, 'leave_type_code' => 'LV-EMG', 'leave_type_name' => 'Emergency Leave', 'annual_balance_days' => 5, 'is_paid' => 1, 'is_active' => 1, 'notes' => 'Manager approval required.'],
            ['company_id' => $companyId, 'leave_type_code' => 'LV-UNP', 'leave_type_name' => 'Unpaid Leave', 'annual_balance_days' => 15, 'is_paid' => 0, 'is_active' => 1, 'notes' => 'Deducted from payroll.'],
            ['company_id' => $companyId, 'leave_type_code' => 'LV-MAT', 'leave_type_name' => 'Maternity Leave', 'annual_balance_days' => 70, 'is_paid' => 1, 'is_active' => 1, 'notes' => 'Policy controlled leave.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_salary_components', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_salary_components', [
            ['company_id' => $companyId, 'component_code' => 'SAL-BASIC', 'component_name' => 'Basic Salary', 'component_type' => 'earning', 'calculation_type' => 'fixed', 'default_amount' => 0, 'percent_value' => 0, 'account_code' => '5100', 'is_taxable' => 0, 'is_active' => 1, 'notes' => 'Main monthly salary.'],
            ['company_id' => $companyId, 'component_code' => 'ALW-HOUS', 'component_name' => 'Housing Allowance', 'component_type' => 'allowance', 'calculation_type' => 'percent', 'default_amount' => 0, 'percent_value' => 25, 'account_code' => '5110', 'is_taxable' => 0, 'is_active' => 1, 'notes' => 'Percentage of basic salary.'],
            ['company_id' => $companyId, 'component_code' => 'ALW-TRN', 'component_name' => 'Transport Allowance', 'component_type' => 'allowance', 'calculation_type' => 'fixed', 'default_amount' => 600, 'percent_value' => 0, 'account_code' => '5110', 'is_taxable' => 0, 'is_active' => 1, 'notes' => 'Monthly transport support.'],
            ['company_id' => $companyId, 'component_code' => 'DED-LATE', 'component_name' => 'Late Attendance Deduction', 'component_type' => 'deduction', 'calculation_type' => 'fixed', 'default_amount' => 150, 'percent_value' => 0, 'account_code' => '5100', 'is_taxable' => 0, 'is_active' => 1, 'notes' => 'Applied when attendance policy requires it.'],
            ['company_id' => $companyId, 'component_code' => 'DED-UNP', 'component_name' => 'Unpaid Leave Deduction', 'component_type' => 'deduction', 'calculation_type' => 'fixed', 'default_amount' => 300, 'percent_value' => 0, 'account_code' => '5100', 'is_taxable' => 0, 'is_active' => 1, 'notes' => 'Used for unpaid leave days.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'projects', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'projects', [
            ['company_id' => $companyId, 'project_code' => 'PRJ001', 'project_name' => 'STEM Lab Upgrade', 'client_name' => 'School Board', 'start_date' => $year . '-01-15', 'end_date' => $year . '-06-30', 'budget_amount' => 180000, 'status' => 'active'],
            ['company_id' => $companyId, 'project_code' => 'PRJ002', 'project_name' => 'Campus WiFi Expansion', 'client_name' => 'IT Committee', 'start_date' => $year . '-02-01', 'end_date' => $year . '-05-31', 'budget_amount' => 95000, 'status' => 'active'],
            ['company_id' => $companyId, 'project_code' => 'PRJ003', 'project_name' => 'Bus Fleet Renewal', 'client_name' => 'Operations', 'start_date' => $year . '-03-10', 'end_date' => $year . '-11-30', 'budget_amount' => 420000, 'status' => 'planned'],
            ['company_id' => $companyId, 'project_code' => 'PRJ004', 'project_name' => 'Sports Hall Maintenance', 'client_name' => 'Facilities', 'start_date' => $year . '-01-20', 'end_date' => $year . '-04-20', 'budget_amount' => 60000, 'status' => 'closed'],
            ['company_id' => $companyId, 'project_code' => 'PRJ005', 'project_name' => 'Teacher Training Program', 'client_name' => 'Academic Affairs', 'start_date' => $year . '-04-01', 'end_date' => $year . '-09-30', 'budget_amount' => 125000, 'status' => 'on_hold'],
        ]);
    }

    if (app_seed_table_count($pdo, 'budgets', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'budgets', [
            ['company_id' => $companyId, 'fiscal_year' => (string) $year, 'budget_name' => 'Academic Operations', 'account_code' => '5100', 'budget_amount' => 1200000, 'notes' => 'Teacher payroll and classrooms'],
            ['company_id' => $companyId, 'fiscal_year' => (string) $year, 'budget_name' => 'Transport Operations', 'account_code' => '5410', 'budget_amount' => 380000, 'notes' => 'Fuel and bus services'],
            ['company_id' => $companyId, 'fiscal_year' => (string) $year, 'budget_name' => 'Digital Transformation', 'account_code' => '5210', 'budget_amount' => 275000, 'notes' => 'LMS and devices'],
            ['company_id' => $companyId, 'fiscal_year' => (string) $year, 'budget_name' => 'Facilities Maintenance', 'account_code' => '5330', 'budget_amount' => 210000, 'notes' => 'Repairs and safety'],
            ['company_id' => $companyId, 'fiscal_year' => (string) $year, 'budget_name' => 'Activities and Events', 'account_code' => '5510', 'budget_amount' => 145000, 'notes' => 'Events and clubs'],
        ]);
    }
    foreach (['4200' => '5410', '6100' => '5210', '6200' => '5330', '6300' => '5510'] as $legacyBudgetCode => $replacementBudgetCode) {
        if (!app_account_is_leaf($companyId, $replacementBudgetCode, 'expense')) {
            continue;
        }
        $pdo->prepare(
            'UPDATE budgets
             SET account_code = :replacement_code
             WHERE company_id = :company_id
               AND account_code = :legacy_code'
        )->execute([
            'replacement_code' => $replacementBudgetCode,
            'company_id' => $companyId,
            'legacy_code' => $legacyBudgetCode,
        ]);
    }

    if (app_seed_table_count($pdo, 'journal_entries', $companyId) === 0) {
        $openingStmt = $pdo->prepare(
            'INSERT INTO journal_entries
             (company_id, entry_number, entry_date, reference, description, total_debit, total_credit, status, source_type, source_id, is_system_generated, created_by_user_id)
             VALUES
             (:company_id, :entry_number, :entry_date, :reference, :description, :total_debit, :total_credit, :status, :source_type, :source_id, :is_system_generated, :created_by_user_id)'
        );
        $openingStmt->execute([
            'company_id' => $companyId,
            'entry_number' => 'JE-OPEN-0001',
            'entry_date' => $today,
            'reference' => 'OPENING',
            'description' => 'Opening balances',
            'total_debit' => 120000,
            'total_credit' => 120000,
            'status' => 'draft',
            'source_type' => 'opening',
            'source_id' => null,
            'is_system_generated' => 1,
            'created_by_user_id' => null,
        ]);
        $openingEntryId = (int) $pdo->lastInsertId();
        app_seed_insert_rows($pdo, 'journal_entry_lines', [
            ['company_id' => $companyId, 'journal_entry_id' => $openingEntryId, 'line_no' => 1, 'account_code' => '1112', 'debit' => 120000, 'credit' => 0, 'description' => 'Opening cash/bank', 'source_type' => 'opening', 'source_id' => null],
            ['company_id' => $companyId, 'journal_entry_id' => $openingEntryId, 'line_no' => 2, 'account_code' => '3100', 'debit' => 0, 'credit' => 120000, 'description' => 'Opening capital', 'source_type' => 'opening', 'source_id' => null],
        ]);
    }

    if (app_seed_table_count($pdo, 'sales_invoices', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'sales_invoices', [
            ['company_id' => $companyId, 'invoice_number' => 'SINV-0001', 'invoice_date' => $today, 'customer_code' => 'CUST001', 'revenue_account_code' => '4100', 'is_taxable' => 0, 'tax_rate_percent' => 0, 'tax_name' => '', 'subtotal' => 12000, 'tax_total' => 0, 'discount_total' => 500, 'grand_total' => 11500, 'inline_paid_amount' => 9000, 'paid_amount' => 9000, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'status' => 'partially_paid', 'notes' => 'Grade 1 tuition term 1'],
            ['company_id' => $companyId, 'invoice_number' => 'SINV-0002', 'invoice_date' => $today, 'customer_code' => 'CUST002', 'revenue_account_code' => '4100', 'is_taxable' => 0, 'tax_rate_percent' => 0, 'tax_name' => '', 'subtotal' => 13200, 'tax_total' => 0, 'discount_total' => 0, 'grand_total' => 13200, 'inline_paid_amount' => 13200, 'paid_amount' => 13200, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'status' => 'paid', 'notes' => 'Grade 2 tuition'],
            ['company_id' => $companyId, 'invoice_number' => 'SINV-0003', 'invoice_date' => $today, 'customer_code' => 'CUST003', 'revenue_account_code' => '4130', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Sales 15%', 'subtotal' => 850, 'tax_total' => 127.5, 'discount_total' => 0, 'grand_total' => 977.5, 'inline_paid_amount' => 500, 'paid_amount' => 500, 'payment_method' => 'cash', 'payment_account_name' => 'Petty Cash Vault', 'status' => 'partially_paid', 'notes' => 'Books package'],
            ['company_id' => $companyId, 'invoice_number' => 'SINV-0004', 'invoice_date' => $today, 'customer_code' => 'CUST001', 'revenue_account_code' => '4120', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Sales 15%', 'subtotal' => 3200, 'tax_total' => 480, 'discount_total' => 0, 'grand_total' => 3680, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'issued', 'notes' => 'Transport package'],
            ['company_id' => $companyId, 'invoice_number' => 'SINV-0005', 'invoice_date' => $today, 'customer_code' => 'CUST002', 'revenue_account_code' => '4140', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Sales 15%', 'subtotal' => 620, 'tax_total' => 93, 'discount_total' => 20, 'grand_total' => 693, 'inline_paid_amount' => 693, 'paid_amount' => 693, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'status' => 'paid', 'notes' => 'Uniform set'],
        ]);
    }

    if (app_seed_table_count($pdo, 'purchase_bills', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'purchase_bills', [
            ['company_id' => $companyId, 'bill_number' => 'PBIL-0001', 'bill_date' => $today, 'vendor_code' => 'VEND001', 'expense_account_code' => '5210', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 5500, 'tax_total' => 825, 'discount_total' => 0, 'grand_total' => 6325, 'inline_paid_amount' => 3000, 'paid_amount' => 3000, 'payment_method' => 'transfer', 'payment_account_name' => 'Main Operating Account', 'status' => 'partially_paid', 'notes' => 'Books procurement'],
            ['company_id' => $companyId, 'bill_number' => 'PBIL-0002', 'bill_date' => $today, 'vendor_code' => 'VEND002', 'expense_account_code' => '5410', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 4100, 'tax_total' => 615, 'discount_total' => 0, 'grand_total' => 4715, 'inline_paid_amount' => 4715, 'paid_amount' => 4715, 'payment_method' => 'transfer', 'payment_account_name' => 'Main Operating Account', 'status' => 'paid', 'notes' => 'Bus service monthly fee'],
            ['company_id' => $companyId, 'bill_number' => 'PBIL-0003', 'bill_date' => $today, 'vendor_code' => 'VEND001', 'expense_account_code' => '5210', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 2800, 'tax_total' => 420, 'discount_total' => 50, 'grand_total' => 3170, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'approved', 'notes' => 'Uniform supplies'],
            ['company_id' => $companyId, 'bill_number' => 'PBIL-0004', 'bill_date' => $today, 'vendor_code' => 'VEND002', 'expense_account_code' => '5330', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 3600, 'tax_total' => 540, 'discount_total' => 0, 'grand_total' => 4140, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'draft', 'notes' => 'Maintenance services'],
            ['company_id' => $companyId, 'bill_number' => 'PBIL-0005', 'bill_date' => $today, 'vendor_code' => 'VEND001', 'expense_account_code' => '5510', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 1500, 'tax_total' => 225, 'discount_total' => 0, 'grand_total' => 1725, 'inline_paid_amount' => 1000, 'paid_amount' => 1000, 'payment_method' => 'cash', 'payment_account_name' => 'Petty Cash Vault', 'status' => 'partially_paid', 'notes' => 'Stationery'],
        ]);
    }
    if ($isExcellenceTarget) {
        $pdo->prepare(
            "UPDATE purchase_bills b
             INNER JOIN contacts c
                ON c.company_id = b.company_id
               AND c.contact_code = b.vendor_code
             SET b.withholding_tax_name = c.withholding_tax_name,
                 b.withholding_tax_rate_percent = c.withholding_tax_rate_percent,
                 b.withholding_tax_total = ROUND(GREATEST(0, b.subtotal - b.discount_total) * (c.withholding_tax_rate_percent / 100), 2),
                 b.net_payable = GREATEST(0, b.grand_total - ROUND(GREATEST(0, b.subtotal - b.discount_total) * (c.withholding_tax_rate_percent / 100), 2))
             WHERE b.company_id = :company_id
               AND c.withholding_tax_name <> ''
               AND (b.withholding_tax_name = '' OR b.withholding_tax_name IS NULL)"
        )->execute(['company_id' => $companyId]);
    }

    if (app_seed_table_count($pdo, 'expenses', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'expenses', [
            ['company_id' => $companyId, 'expense_number' => 'EXP-0001', 'expense_date' => $today, 'payee_code' => 'VEND001', 'expense_account_code' => '5330', 'cost_center_code' => 'CC-ADM', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 950, 'tax_total' => 142.5, 'discount_total' => 0, 'grand_total' => 1092.5, 'inline_paid_amount' => 1092.5, 'paid_amount' => 1092.5, 'payment_method' => 'cash', 'payment_account_name' => 'Petty Cash Vault', 'status' => 'paid', 'notes' => 'Minor maintenance supplies'],
            ['company_id' => $companyId, 'expense_number' => 'EXP-0002', 'expense_date' => $today, 'payee_code' => 'VEND002', 'expense_account_code' => '5210', 'cost_center_code' => 'CC-PRI', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 1800, 'tax_total' => 270, 'discount_total' => 0, 'grand_total' => 2070, 'inline_paid_amount' => 0, 'paid_amount' => 0, 'payment_method' => '', 'payment_account_name' => '', 'status' => 'approved', 'notes' => 'Learning software monthly subscription'],
            ['company_id' => $companyId, 'expense_number' => 'EXP-0003', 'expense_date' => $today, 'payee_code' => 'VEND001', 'expense_account_code' => '5510', 'cost_center_code' => 'CC-ACT', 'is_taxable' => 0, 'tax_rate_percent' => 0, 'tax_name' => '', 'subtotal' => 1250, 'tax_total' => 0, 'discount_total' => 0, 'grand_total' => 1250, 'inline_paid_amount' => 1250, 'paid_amount' => 1250, 'payment_method' => 'cash', 'payment_account_name' => 'Activities Cash Box', 'status' => 'paid', 'notes' => 'Student event materials'],
            ['company_id' => $companyId, 'expense_number' => 'EXP-0004', 'expense_date' => $today, 'payee_code' => 'VEND002', 'expense_account_code' => '5410', 'cost_center_code' => 'CC-TRN', 'is_taxable' => 1, 'tax_rate_percent' => 15, 'tax_name' => 'VAT Purchase 15%', 'subtotal' => 2200, 'tax_total' => 330, 'discount_total' => 0, 'grand_total' => 2530, 'inline_paid_amount' => 1000, 'paid_amount' => 1000, 'payment_method' => 'bank', 'payment_account_name' => 'Main Operating Account', 'status' => 'partially_paid', 'notes' => 'Fuel and route costs'],
            ['company_id' => $companyId, 'expense_number' => 'EXP-0005', 'expense_date' => $today, 'payee_code' => '', 'expense_account_code' => '5510', 'cost_center_code' => 'CC-ADM', 'is_taxable' => 0, 'tax_rate_percent' => 0, 'tax_name' => '', 'subtotal' => 420, 'tax_total' => 0, 'discount_total' => 0, 'grand_total' => 420, 'inline_paid_amount' => 420, 'paid_amount' => 420, 'payment_method' => 'cash', 'payment_account_name' => 'Reception Cash Box', 'status' => 'paid', 'notes' => 'Office refreshments'],
        ]);
    }

    if (app_seed_table_count($pdo, 'payments', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'payments', [
            ['company_id' => $companyId, 'payment_number' => 'PAY-0001', 'payment_date' => $today, 'direction' => 'in', 'payment_method' => 'bank', 'amount' => 9000, 'reference' => 'SINV-0001', 'purchase_bill_id' => null, 'contact_code' => 'CUST001', 'bank_account_name' => 'Main Operating Account', 'offset_account_code' => '1121', 'notes' => 'Part tuition collection'],
            ['company_id' => $companyId, 'payment_number' => 'PAY-0002', 'payment_date' => $today, 'direction' => 'in', 'payment_method' => 'bank', 'amount' => 13200, 'reference' => 'SINV-0002', 'purchase_bill_id' => null, 'contact_code' => 'CUST002', 'bank_account_name' => 'Main Operating Account', 'offset_account_code' => '1121', 'notes' => 'Full tuition collection'],
            ['company_id' => $companyId, 'payment_number' => 'PAY-0003', 'payment_date' => $today, 'direction' => 'out', 'payment_method' => 'transfer', 'amount' => 3000, 'reference' => 'PBIL-0001', 'purchase_bill_id' => null, 'contact_code' => 'VEND001', 'bank_account_name' => 'Main Operating Account', 'offset_account_code' => '2111', 'notes' => 'Partial vendor payment'],
            ['company_id' => $companyId, 'payment_number' => 'PAY-0004', 'payment_date' => $today, 'direction' => 'out', 'payment_method' => 'transfer', 'amount' => 4715, 'reference' => 'PBIL-0002', 'purchase_bill_id' => null, 'contact_code' => 'VEND002', 'bank_account_name' => 'Main Operating Account', 'offset_account_code' => '2111', 'notes' => 'Bill settlement'],
            ['company_id' => $companyId, 'payment_number' => 'PAY-0005', 'payment_date' => $today, 'direction' => 'in', 'payment_method' => 'cash', 'amount' => 500, 'reference' => 'SINV-0003', 'purchase_bill_id' => null, 'contact_code' => 'CUST003', 'bank_account_name' => 'Petty Cash Vault', 'offset_account_code' => '1121', 'notes' => 'Books collection'],
        ]);
    }
    $pdo->prepare(
        "UPDATE payments p
         INNER JOIN purchase_bills b
            ON b.company_id = p.company_id
           AND b.bill_number = p.reference
         SET p.purchase_bill_id = b.id
         WHERE p.company_id = :company_id
           AND p.direction = 'out'
           AND (p.purchase_bill_id IS NULL OR p.purchase_bill_id = 0)"
    )->execute(['company_id' => $companyId]);
    $pdo->prepare(
        "UPDATE payments p
         INNER JOIN expenses e
            ON e.company_id = p.company_id
           AND e.expense_number = p.reference
         SET p.expense_id = e.id
         WHERE p.company_id = :company_id
           AND p.direction = 'out'
           AND (p.expense_id IS NULL OR p.expense_id = 0)"
    )->execute(['company_id' => $companyId]);

    if (app_seed_table_count($pdo, 'assets', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'assets', [
            ['company_id' => $companyId, 'asset_code' => 'AST-001', 'asset_name' => 'School Buses Fleet', 'purchase_date' => ($year - 3) . '-09-01', 'purchase_cost' => 850000, 'useful_life_months' => 120, 'salvage_value' => 120000, 'depreciation_method' => 'straight_line', 'accumulated_depreciation' => 182000, 'status' => 'active'],
            ['company_id' => $companyId, 'asset_code' => 'AST-002', 'asset_name' => 'Computer Lab Devices', 'purchase_date' => ($year - 2) . '-01-10', 'purchase_cost' => 240000, 'useful_life_months' => 60, 'salvage_value' => 20000, 'depreciation_method' => 'straight_line', 'accumulated_depreciation' => 96000, 'status' => 'active'],
            ['company_id' => $companyId, 'asset_code' => 'AST-003', 'asset_name' => 'Classroom Furniture', 'purchase_date' => ($year - 4) . '-08-20', 'purchase_cost' => 170000, 'useful_life_months' => 96, 'salvage_value' => 12000, 'depreciation_method' => 'straight_line', 'accumulated_depreciation' => 99000, 'status' => 'active'],
            ['company_id' => $companyId, 'asset_code' => 'AST-004', 'asset_name' => 'Sports Equipment', 'purchase_date' => ($year - 1) . '-03-15', 'purchase_cost' => 45000, 'useful_life_months' => 48, 'salvage_value' => 3500, 'depreciation_method' => 'declining', 'accumulated_depreciation' => 11000, 'status' => 'under_maintenance'],
            ['company_id' => $companyId, 'asset_code' => 'AST-005', 'asset_name' => 'Old Printer Set', 'purchase_date' => ($year - 6) . '-02-01', 'purchase_cost' => 16000, 'useful_life_months' => 36, 'salvage_value' => 500, 'depreciation_method' => 'straight_line', 'accumulated_depreciation' => 15500, 'status' => 'disposed'],
        ]);
    }

    if (app_seed_table_count($pdo, 'employees', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'employees', [
            ['company_id' => $companyId, 'employee_number' => 'EMP001', 'full_name' => 'Mona Saeed', 'department_code' => 'HR-ADM', 'job_code' => 'JOB-PRN', 'department' => 'Administration', 'basic_salary' => 8500, 'allowance' => 1200, 'deduction_default' => 300, 'is_active' => 1],
            ['company_id' => $companyId, 'employee_number' => 'EMP002', 'full_name' => 'Yousef Nasser', 'department_code' => 'HR-PRI', 'job_code' => 'JOB-TCH', 'department' => 'Primary School', 'basic_salary' => 9200, 'allowance' => 900, 'deduction_default' => 250, 'is_active' => 1],
            ['company_id' => $companyId, 'employee_number' => 'EMP003', 'full_name' => 'Huda Adel', 'department_code' => 'HR-SEC', 'job_code' => 'JOB-SUP', 'department' => 'Secondary School', 'basic_salary' => 9800, 'allowance' => 1100, 'deduction_default' => 350, 'is_active' => 1],
            ['company_id' => $companyId, 'employee_number' => 'EMP004', 'full_name' => 'Fahad Salem', 'department_code' => 'HR-TRN', 'job_code' => 'JOB-DRV', 'department' => 'Transportation', 'basic_salary' => 10500, 'allowance' => 1500, 'deduction_default' => 400, 'is_active' => 1],
            ['company_id' => $companyId, 'employee_number' => 'EMP005', 'full_name' => 'Rania Khaled', 'department_code' => 'HR-FIN', 'job_code' => 'JOB-ACC', 'department' => 'Finance', 'basic_salary' => 11200, 'allowance' => 1800, 'deduction_default' => 450, 'is_active' => 1],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_employee_contracts', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_employee_contracts', [
            ['company_id' => $companyId, 'contract_number' => 'CTR-0001', 'employee_number' => 'EMP001', 'department_code' => 'HR-ADM', 'job_code' => 'JOB-PRN', 'contract_type' => 'full_time', 'start_date' => ($year - 2) . '-08-01', 'end_date' => null, 'basic_salary' => 8500, 'allowance' => 1200, 'insurance_number' => 'INS-1001', 'status' => 'active', 'notes' => 'Leadership contract.'],
            ['company_id' => $companyId, 'contract_number' => 'CTR-0002', 'employee_number' => 'EMP002', 'department_code' => 'HR-PRI', 'job_code' => 'JOB-TCH', 'contract_type' => 'full_time', 'start_date' => ($year - 1) . '-08-15', 'end_date' => null, 'basic_salary' => 9200, 'allowance' => 900, 'insurance_number' => 'INS-1002', 'status' => 'active', 'notes' => 'Primary teacher contract.'],
            ['company_id' => $companyId, 'contract_number' => 'CTR-0003', 'employee_number' => 'EMP003', 'department_code' => 'HR-SEC', 'job_code' => 'JOB-SUP', 'contract_type' => 'full_time', 'start_date' => ($year - 3) . '-08-15', 'end_date' => null, 'basic_salary' => 9800, 'allowance' => 1100, 'insurance_number' => 'INS-1003', 'status' => 'active', 'notes' => 'Academic supervisor.'],
            ['company_id' => $companyId, 'contract_number' => 'CTR-0004', 'employee_number' => 'EMP004', 'department_code' => 'HR-TRN', 'job_code' => 'JOB-DRV', 'contract_type' => 'full_time', 'start_date' => ($year - 1) . '-09-01', 'end_date' => null, 'basic_salary' => 10500, 'allowance' => 1500, 'insurance_number' => 'INS-1004', 'status' => 'active', 'notes' => 'Transport team.'],
            ['company_id' => $companyId, 'contract_number' => 'CTR-0005', 'employee_number' => 'EMP005', 'department_code' => 'HR-FIN', 'job_code' => 'JOB-ACC', 'contract_type' => 'full_time', 'start_date' => ($year - 2) . '-01-01', 'end_date' => null, 'basic_salary' => 11200, 'allowance' => 1800, 'insurance_number' => 'INS-1005', 'status' => 'active', 'notes' => 'Finance and payroll support.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_attendance', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_attendance', [
            ['company_id' => $companyId, 'attendance_number' => 'ATT-0001', 'employee_number' => 'EMP001', 'shift_code' => 'SH-DAY', 'attendance_date' => $today, 'check_in' => '08:00:00', 'check_out' => '16:00:00', 'status' => 'present', 'hours_worked' => 8, 'notes' => 'On time'],
            ['company_id' => $companyId, 'attendance_number' => 'ATT-0002', 'employee_number' => 'EMP002', 'shift_code' => 'SH-DAY', 'attendance_date' => $today, 'check_in' => '08:15:00', 'check_out' => '16:05:00', 'status' => 'late', 'hours_worked' => 7.83, 'notes' => 'Late arrival'],
            ['company_id' => $companyId, 'attendance_number' => 'ATT-0003', 'employee_number' => 'EMP003', 'shift_code' => 'SH-DAY', 'attendance_date' => $today, 'check_in' => '08:00:00', 'check_out' => '15:55:00', 'status' => 'present', 'hours_worked' => 7.92, 'notes' => 'Regular day'],
            ['company_id' => $companyId, 'attendance_number' => 'ATT-0004', 'employee_number' => 'EMP004', 'shift_code' => 'SH-BUS-AM', 'attendance_date' => $today, 'check_in' => null, 'check_out' => null, 'status' => 'absent', 'hours_worked' => 0, 'notes' => 'No show'],
            ['company_id' => $companyId, 'attendance_number' => 'ATT-0005', 'employee_number' => 'EMP005', 'shift_code' => 'SH-DAY', 'attendance_date' => $today, 'check_in' => '08:05:00', 'check_out' => '16:10:00', 'status' => 'present', 'hours_worked' => 8.08, 'notes' => 'Regular day'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_leave_requests', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_leave_requests', [
            ['company_id' => $companyId, 'request_number' => 'LV-0001', 'employee_number' => 'EMP001', 'leave_type_code' => 'LV-ANN', 'leave_type' => 'annual', 'start_date' => $year . '-02-10', 'end_date' => $year . '-02-12', 'days_count' => 3, 'status' => 'approved', 'notes' => 'Family leave'],
            ['company_id' => $companyId, 'request_number' => 'LV-0002', 'employee_number' => 'EMP002', 'leave_type_code' => 'LV-SICK', 'leave_type' => 'sick', 'start_date' => $year . '-03-05', 'end_date' => $year . '-03-05', 'days_count' => 1, 'status' => 'approved', 'notes' => 'Medical note received'],
            ['company_id' => $companyId, 'request_number' => 'LV-0003', 'employee_number' => 'EMP003', 'leave_type_code' => 'LV-ANN', 'leave_type' => 'annual', 'start_date' => $year . '-04-01', 'end_date' => $year . '-04-04', 'days_count' => 4, 'status' => 'pending', 'notes' => 'Pending manager approval'],
            ['company_id' => $companyId, 'request_number' => 'LV-0004', 'employee_number' => 'EMP004', 'leave_type_code' => 'LV-EMG', 'leave_type' => 'emergency', 'start_date' => $year . '-05-14', 'end_date' => $year . '-05-14', 'days_count' => 1, 'status' => 'draft', 'notes' => 'Draft request'],
            ['company_id' => $companyId, 'request_number' => 'LV-0005', 'employee_number' => 'EMP005', 'leave_type_code' => 'LV-ANN', 'leave_type' => 'annual', 'start_date' => $year . '-06-20', 'end_date' => $year . '-06-22', 'days_count' => 3, 'status' => 'rejected', 'notes' => 'Payroll close period'],
        ]);
    }

    if (app_seed_table_count($pdo, 'payroll_runs', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'payroll_runs', [
            ['company_id' => $companyId, 'run_number' => 'PR-2026-01', 'period_month' => 1, 'period_year' => $year, 'total_gross' => 250000, 'total_deductions' => 15000, 'total_net' => 235000, 'status' => 'posted'],
            ['company_id' => $companyId, 'run_number' => 'PR-2026-02', 'period_month' => 2, 'period_year' => $year, 'total_gross' => 252500, 'total_deductions' => 15100, 'total_net' => 237400, 'status' => 'posted'],
            ['company_id' => $companyId, 'run_number' => 'PR-2026-03', 'period_month' => 3, 'period_year' => $year, 'total_gross' => 251700, 'total_deductions' => 14980, 'total_net' => 236720, 'status' => 'approved'],
            ['company_id' => $companyId, 'run_number' => 'PR-2026-04', 'period_month' => 4, 'period_year' => $year, 'total_gross' => 255000, 'total_deductions' => 15200, 'total_net' => 239800, 'status' => 'draft'],
            ['company_id' => $companyId, 'run_number' => 'PR-2026-05', 'period_month' => 5, 'period_year' => $year, 'total_gross' => 257300, 'total_deductions' => 15400, 'total_net' => 241900, 'status' => 'draft'],
        ]);
    }

    if (app_seed_table_count($pdo, 'hr_payroll_items', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'hr_payroll_items', [
            ['company_id' => $companyId, 'item_number' => 'PRI-0001', 'run_number' => 'PR-2026-01', 'employee_number' => 'EMP001', 'component_code' => 'SAL-BASIC', 'component_type' => 'earning', 'amount' => 8500, 'account_code' => '5100', 'cost_center_code' => 'CC-ADM', 'notes' => 'January basic salary.'],
            ['company_id' => $companyId, 'item_number' => 'PRI-0002', 'run_number' => 'PR-2026-01', 'employee_number' => 'EMP002', 'component_code' => 'SAL-BASIC', 'component_type' => 'earning', 'amount' => 9200, 'account_code' => '5100', 'cost_center_code' => 'CC-PRI', 'notes' => 'January basic salary.'],
            ['company_id' => $companyId, 'item_number' => 'PRI-0003', 'run_number' => 'PR-2026-01', 'employee_number' => 'EMP002', 'component_code' => 'ALW-HOUS', 'component_type' => 'allowance', 'amount' => 2300, 'account_code' => '5110', 'cost_center_code' => 'CC-PRI', 'notes' => 'Housing allowance.'],
            ['company_id' => $companyId, 'item_number' => 'PRI-0004', 'run_number' => 'PR-2026-01', 'employee_number' => 'EMP004', 'component_code' => 'ALW-TRN', 'component_type' => 'allowance', 'amount' => 600, 'account_code' => '5110', 'cost_center_code' => 'CC-TRN', 'notes' => 'Transport allowance.'],
            ['company_id' => $companyId, 'item_number' => 'PRI-0005', 'run_number' => 'PR-2026-01', 'employee_number' => 'EMP004', 'component_code' => 'DED-LATE', 'component_type' => 'deduction', 'amount' => 150, 'account_code' => '5100', 'cost_center_code' => 'CC-TRN', 'notes' => 'Attendance deduction.'],
        ]);
    }

    if (app_seed_table_count($pdo, 'inventory_movements', $companyId) < 5) {
        app_seed_insert_rows($pdo, 'inventory_movements', [
            ['company_id' => $companyId, 'movement_number' => 'INV-MV-001', 'movement_date' => $today, 'movement_type' => 'in', 'sku' => 'BOOKS-SET', 'quantity' => 120, 'unit_cost' => 500, 'reference' => 'PBIL-0001', 'notes' => 'Books received'],
            ['company_id' => $companyId, 'movement_number' => 'INV-MV-002', 'movement_date' => $today, 'movement_type' => 'in', 'sku' => 'UNIFORM-SET', 'quantity' => 90, 'unit_cost' => 380, 'reference' => 'PBIL-0003', 'notes' => 'Uniform stock received'],
            ['company_id' => $companyId, 'movement_number' => 'INV-MV-003', 'movement_date' => $today, 'movement_type' => 'out', 'sku' => 'BOOKS-SET', 'quantity' => 8, 'unit_cost' => 500, 'reference' => 'SINV-0003', 'notes' => 'Sold to students'],
            ['company_id' => $companyId, 'movement_number' => 'INV-MV-004', 'movement_date' => $today, 'movement_type' => 'out', 'sku' => 'UNIFORM-SET', 'quantity' => 5, 'unit_cost' => 380, 'reference' => 'SINV-0005', 'notes' => 'Sold to students'],
            ['company_id' => $companyId, 'movement_number' => 'INV-MV-005', 'movement_date' => $today, 'movement_type' => 'adjustment', 'sku' => 'BOOKS-SET', 'quantity' => -1, 'unit_cost' => 500, 'reference' => 'ADJ-0001', 'notes' => 'Damaged item'],
        ]);
    }

    if (app_seed_table_count($pdo, 'report_templates', $companyId) < 5) {
        $templateRows = [
            [
                'company_id' => $companyId,
                'template_name' => 'Sales Collection Snapshot',
                'module_scope' => 'sales_invoices',
                'chart_type' => 'bar',
                'config_json' => json_encode([
                    'sections' => [[
                        'title' => 'Sales vs Paid Amount',
                        'module' => 'sales_invoices',
                        'label_column' => 'invoice_number',
                        'metric_columns' => ['grand_total', 'paid_amount'],
                        'chart_type' => 'bar',
                        'style' => 'classic',
                    ]],
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                'is_active' => 1,
            ],
            [
                'company_id' => $companyId,
                'template_name' => 'Purchases Overview',
                'module_scope' => 'purchase_bills',
                'chart_type' => 'line',
                'config_json' => json_encode([
                    'sections' => [[
                        'title' => 'Bills vs Paid',
                        'module' => 'purchase_bills',
                        'label_column' => 'bill_number',
                        'metric_columns' => ['grand_total', 'paid_amount'],
                        'chart_type' => 'line',
                        'style' => 'glass',
                    ]],
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                'is_active' => 1,
            ],
            [
                'company_id' => $companyId,
                'template_name' => 'Payroll Summary',
                'module_scope' => 'payroll_runs',
                'chart_type' => 'bar',
                'config_json' => json_encode([
                    'sections' => [[
                        'title' => 'Gross vs Net Payroll',
                        'module' => 'payroll_runs',
                        'label_column' => 'run_number',
                        'metric_columns' => ['total_gross', 'total_net'],
                        'chart_type' => 'bar',
                        'style' => 'royal',
                    ]],
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                'is_active' => 1,
            ],
            [
                'company_id' => $companyId,
                'template_name' => 'Inventory Value',
                'module_scope' => 'inventory_movements',
                'chart_type' => 'doughnut',
                'config_json' => json_encode([
                    'sections' => [[
                        'title' => 'Inventory Cost by SKU',
                        'module' => 'inventory_movements',
                        'label_column' => 'sku',
                        'metric_columns' => ['quantity', 'unit_cost'],
                        'chart_type' => 'doughnut',
                        'style' => 'forest',
                    ]],
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                'is_active' => 1,
            ],
            [
                'company_id' => $companyId,
                'template_name' => 'Cashflow In vs Out',
                'module_scope' => 'payments',
                'chart_type' => 'pie',
                'config_json' => json_encode([
                    'sections' => [[
                        'title' => 'Incoming vs Outgoing',
                        'module' => 'payments',
                        'label_column' => 'payment_method',
                        'metric_columns' => ['amount'],
                        'chart_type' => 'pie',
                        'style' => 'sunset',
                    ]],
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                'is_active' => 1,
            ],
        ];
        app_seed_insert_rows($pdo, 'report_templates', $templateRows);
    }

    app_normalize_document_expense_accounts($pdo, $companyId);
    app_normalize_expense_offset_accounts($pdo, $companyId);
    if ($isExcellenceTarget) {
        $pdo->prepare(
            "UPDATE sales_invoices
             SET revenue_account_code = CASE
                 WHEN revenue_account_code = '4100' THEN '4110'
                 WHEN revenue_account_code = '4200' THEN '4210'
                 ELSE revenue_account_code
             END
             WHERE company_id = :company_id
               AND revenue_account_code IN ('4100', '4200')"
        )->execute(['company_id' => $companyId]);
        $pdo->prepare(
            "UPDATE student_invoices
             SET revenue_account_code = CASE
                 WHEN revenue_account_code = '4100' THEN '4110'
                 WHEN revenue_account_code = '4200' THEN '4210'
                 ELSE revenue_account_code
             END
             WHERE company_id = :company_id
               AND revenue_account_code IN ('4100', '4200')"
        )->execute(['company_id' => $companyId]);
        $pdo->prepare(
            "UPDATE student_revenue_types
             SET revenue_account_code = CASE
                 WHEN revenue_account_code = '4100' THEN '4110'
                 WHEN revenue_account_code = '4200' THEN '4210'
                 ELSE revenue_account_code
             END
             WHERE company_id = :company_id
               AND revenue_account_code IN ('4100', '4200')"
        )->execute(['company_id' => $companyId]);
        $pdo->prepare(
            "UPDATE student_account_settings
             SET default_revenue_account_code = '4110'
             WHERE company_id = :company_id
               AND default_revenue_account_code = '4100'"
        )->execute(['company_id' => $companyId]);
        $pdo->prepare(
            "UPDATE hr_salary_components
             SET account_code = '5210'
             WHERE company_id = :company_id
               AND account_code = '5100'"
        )->execute(['company_id' => $companyId]);
    }

    foreach (['sales_invoices', 'student_invoices', 'student_receipts', 'student_refunds', 'purchase_bills', 'expenses', 'payments', 'excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'] as $sourceTable) {
        $numberColumn = match ($sourceTable) {
            'sales_invoices' => 'invoice_number',
            'student_invoices' => 'student_invoice_number',
            'student_receipts' => 'receipt_number',
            'student_refunds' => 'voucher_number',
            'purchase_bills' => 'bill_number',
            'expenses' => 'expense_number',
            'excellence_operations' => 'operation_number',
            'excellence_hospital_expenses' => 'expense_number',
            'excellence_admin_expenses' => 'expense_number',
            'excellence_salaries' => 'salary_number',
            default => 'payment_number',
        };
        $stmt = $pdo->prepare(
            'SELECT *
             FROM `' . $sourceTable . '`
             WHERE company_id = :company_id
             ORDER BY `' . $numberColumn . '` ASC'
        );
        $stmt->execute(['company_id' => $companyId]);
        foreach (($stmt->fetchAll() ?: []) as $record) {
            app_sync_auto_posting_for_record($sourceTable, $record, $companyId);
        }
    }
}

function app_sanitize_date(string $value, ?string $fallback = null): string
{
    $value = app_normalize_whitespace($value);
    $timestamp = $value !== '' ? strtotime($value) : false;
    if ($timestamp === false) {
        $timestamp = $fallback !== null ? strtotime($fallback) : false;
    }
    if ($timestamp === false) {
        $timestamp = time();
    }
    return date('Y-m-d', $timestamp);
}

function app_chart_accounts(int $companyId, ?string $accountType = null, bool $onlyActive = true): array
{
    if ($companyId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $sql = 'SELECT account_code, account_name, account_type, parent_code, opening_balance, financial_statement_group_code, is_active
            FROM chart_of_accounts
            WHERE company_id = :company_id';
    $params = ['company_id' => $companyId];

    if ($accountType !== null && $accountType !== '') {
        $sql .= ' AND account_type = :account_type';
        $params['account_type'] = $accountType;
    }
    if ($onlyActive) {
        $sql .= ' AND is_active = 1';
    }
    $sql .= ' ORDER BY account_code ASC';

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll() ?: [];
}

function app_account_has_children(int $companyId, string $accountCode): bool
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        return false;
    }

    $stmt = app_pdo()->prepare(
        'SELECT 1
         FROM chart_of_accounts
         WHERE company_id = :company_id
           AND parent_code = :parent_code
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'parent_code' => $accountCode,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_account_is_leaf(int $companyId, string $accountCode, ?string $accountType = null, bool $onlyActive = true): bool
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        return false;
    }

    $pdo = app_pdo();
    $sql = 'SELECT 1
            FROM chart_of_accounts
            WHERE company_id = :company_id
              AND account_code = :account_code';
    $params = [
        'company_id' => $companyId,
        'account_code' => $accountCode,
    ];
    if ($accountType !== null && $accountType !== '') {
        $sql .= ' AND account_type = :account_type';
        $params['account_type'] = $accountType;
    }
    if ($onlyActive) {
        $sql .= ' AND is_active = 1';
    }
    $sql .= ' LIMIT 1';

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    if (!$stmt->fetchColumn()) {
        return false;
    }

    return !app_account_has_children($companyId, $accountCode);
}

function app_assert_leaf_account(int $companyId, string $accountCode, ?string $accountType = null, string $label = 'Account'): void
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        throw new RuntimeException($label . ' is required.');
    }
    if (!app_account_exists($companyId, $accountCode, $accountType)) {
        throw new RuntimeException('Select a valid ' . strtolower($label) . '.');
    }
    if (app_account_has_children($companyId, $accountCode)) {
        throw new RuntimeException($label . ' "' . $accountCode . '" is a parent account. Transactions are allowed on sub accounts only.');
    }
}

function app_financial_statement_main_sections(): array
{
    return [
        'bs_non_current_assets' => [
            'statement_type' => 'balance_sheet',
            'label' => 'Non-current Assets',
            'total_label' => 'Total Non-current Assets',
            'account_types' => ['asset'],
            'sort_order' => 10,
            'presentation_sign' => 1,
        ],
        'bs_current_assets' => [
            'statement_type' => 'balance_sheet',
            'label' => 'Current Assets',
            'total_label' => 'Total Current Assets',
            'account_types' => ['asset'],
            'sort_order' => 20,
            'presentation_sign' => 1,
        ],
        'bs_equity' => [
            'statement_type' => 'balance_sheet',
            'label' => 'Equity',
            'total_label' => 'Total Equity',
            'account_types' => ['equity'],
            'sort_order' => 30,
            'presentation_sign' => 1,
        ],
        'bs_non_current_liabilities' => [
            'statement_type' => 'balance_sheet',
            'label' => 'Non-current Liabilities',
            'total_label' => 'Total Non-current Liabilities',
            'account_types' => ['liability'],
            'sort_order' => 40,
            'presentation_sign' => 1,
        ],
        'bs_current_liabilities' => [
            'statement_type' => 'balance_sheet',
            'label' => 'Current Liabilities',
            'total_label' => 'Total Current Liabilities',
            'account_types' => ['liability'],
            'sort_order' => 50,
            'presentation_sign' => 1,
        ],
        'is_revenue' => [
            'statement_type' => 'income_statement',
            'label' => 'Revenue',
            'total_label' => 'Total Revenue',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 10,
            'presentation_sign' => 1,
        ],
        'is_revenue_deductions' => [
            'statement_type' => 'income_statement',
            'label' => 'Revenue Deductions',
            'total_label' => 'Total Revenue Deductions',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 20,
            'presentation_sign' => -1,
        ],
        'is_cost_of_revenue' => [
            'statement_type' => 'income_statement',
            'label' => 'Cost of Revenue',
            'total_label' => 'Total Cost of Revenue',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 30,
            'presentation_sign' => -1,
        ],
        'is_operating_expenses' => [
            'statement_type' => 'income_statement',
            'label' => 'Operating Expenses',
            'total_label' => 'Total Operating Expenses',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 40,
            'presentation_sign' => -1,
        ],
        'is_other_income' => [
            'statement_type' => 'income_statement',
            'label' => 'Other Income',
            'total_label' => 'Total Other Income',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 50,
            'presentation_sign' => 1,
        ],
        'is_finance_costs' => [
            'statement_type' => 'income_statement',
            'label' => 'Finance Costs',
            'total_label' => 'Total Finance Costs',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 60,
            'presentation_sign' => -1,
        ],
        'is_income_tax' => [
            'statement_type' => 'income_statement',
            'label' => 'Income Tax',
            'total_label' => 'Total Income Tax',
            'account_types' => ['revenue', 'expense'],
            'sort_order' => 70,
            'presentation_sign' => -1,
        ],
    ];
}

function app_financial_statement_main_section_options(?string $statementType = null): array
{
    $statementType = $statementType !== null ? app_normalize_whitespace($statementType) : null;
    $options = [];
    foreach (app_financial_statement_main_sections() as $sectionCode => $section) {
        if ($statementType !== null && $statementType !== '' && (string) ($section['statement_type'] ?? '') !== $statementType) {
            continue;
        }
        $options[$sectionCode] = app_t((string) ($section['label'] ?? $sectionCode));
    }
    return $options;
}

function app_financial_statement_section_label(string $sectionCode, bool $total = false): string
{
    $section = app_financial_statement_main_sections()[$sectionCode] ?? null;
    if (!$section) {
        return $sectionCode;
    }
    $key = $total ? (string) ($section['total_label'] ?? $section['label'] ?? $sectionCode) : (string) ($section['label'] ?? $sectionCode);
    return app_t($key);
}

function app_financial_statement_section_for_account_type(string $accountType, string $statementType): string
{
    $accountType = strtolower(app_normalize_whitespace($accountType));
    $statementType = app_normalize_whitespace($statementType);
    if ($statementType === 'income_statement') {
        return $accountType === 'revenue' ? 'is_revenue' : 'is_operating_expenses';
    }
    return match ($accountType) {
        'asset' => 'bs_current_assets',
        'liability' => 'bs_current_liabilities',
        'equity' => 'bs_equity',
        default => '',
    };
}

function app_financial_statement_group_by_code(int $companyId, string $groupCode, bool $onlyActive = true): ?array
{
    $groupCode = app_normalize_whitespace($groupCode);
    if ($companyId <= 0 || $groupCode === '') {
        return null;
    }

    $sql = 'SELECT *
            FROM financial_statement_groups
            WHERE company_id = :company_id
              AND group_code = :group_code';
    if ($onlyActive) {
        $sql .= ' AND is_active = 1';
    }
    $sql .= ' LIMIT 1';
    $stmt = app_pdo()->prepare($sql);
    $stmt->execute([
        'company_id' => $companyId,
        'group_code' => $groupCode,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_financial_statement_group_options(int $companyId, ?string $statementType = null): array
{
    if ($companyId <= 0) {
        return [];
    }

    $statementType = $statementType !== null ? app_normalize_whitespace($statementType) : '';
    $sql = 'SELECT group_code, group_name, statement_type, main_section, sort_order
            FROM financial_statement_groups
            WHERE company_id = :company_id
              AND is_active = 1';
    $params = ['company_id' => $companyId];
    if ($statementType !== '') {
        $sql .= ' AND statement_type = :statement_type';
        $params['statement_type'] = $statementType;
    }
    $sql .= ' ORDER BY statement_type ASC, main_section ASC, sort_order ASC, group_code ASC';

    $stmt = app_pdo()->prepare($sql);
    $stmt->execute($params);
    $options = [];
    foreach (($stmt->fetchAll() ?: []) as $row) {
        $code = app_normalize_whitespace((string) ($row['group_code'] ?? ''));
        if ($code === '') {
            continue;
        }
        $section = app_financial_statement_section_label((string) ($row['main_section'] ?? ''));
        $options[$code] = $code . ' - ' . (string) ($row['group_name'] ?? '') . ($section !== '' ? ' - ' . $section : '');
    }
    return $options;
}

function app_module_lookup_options(string $lookup, ?int $companyId): array
{
    $pdo = app_pdo();

    $build = static function (array $rows, string $valueKey, callable $labelFn): array {
        $options = [];
        foreach ($rows as $row) {
            $value = app_normalize_whitespace((string) ($row[$valueKey] ?? ''));
            if ($value === '') {
                continue;
            }
            $options[$value] = $labelFn($row, $value);
        }
        return $options;
    };

    if ($lookup === 'module_slug') {
        return app_permission_module_options();
    }

    if ($lookup === 'role_code') {
        try {
            $stmt = $pdo->query('SELECT role_code, role_name FROM roles WHERE is_active = 1 ORDER BY role_name ASC, role_code ASC');
            return $build(
                $stmt->fetchAll() ?: [],
                'role_code',
                static fn (array $row, string $value): string => app_ui_language() === 'ar'
                    ? app_t((string) ($row['role_name'] ?? $value))
                    : ($value . ' - ' . (string) ($row['role_name'] ?? ''))
            );
        } catch (Throwable) {
            return app_ui_language() === 'ar'
                ? ['admin' => app_t('Administrator'), 'accountant' => app_t('Accountant'), 'auditor' => app_t('Auditor')]
                : ['admin' => 'admin - Administrator', 'accountant' => 'accountant - Accountant', 'auditor' => 'auditor - Auditor'];
        }
    }

    if ($companyId === null || $companyId <= 0) {
        return [];
    }

    switch ($lookup) {
        case 'chart_account_code':
        case 'chart_parent_code':
        case 'asset_account_code':
        case 'revenue_account_code':
        case 'expense_account_code':
        case 'liability_account_code':
        case 'equity_account_code':
            $typeFilter = match ($lookup) {
                'asset_account_code' => 'asset',
                'revenue_account_code' => 'revenue',
                'expense_account_code' => 'expense',
                'liability_account_code' => 'liability',
                'equity_account_code' => 'equity',
                default => null,
            };
            $accounts = app_chart_accounts($companyId, $typeFilter, true);
            if ($lookup !== 'chart_parent_code') {
                $accounts = array_values(array_filter(
                    $accounts,
                    static fn (array $row): bool => !app_account_has_children($companyId, (string) ($row['account_code'] ?? ''))
                ));
            }
            return $build(
                $accounts,
                'account_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['account_name'] ?? '')
            );

        case 'financial_statement_group_code':
            return app_financial_statement_group_options($companyId);

        case 'ecl_country_code':
            $stmt = $pdo->prepare(
                'SELECT country_code, country_name
                 FROM ecl_countries
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY country_name ASC, country_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            return $build(
                $stmt->fetchAll() ?: [],
                'country_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['country_name'] ?? '')
            );

        case 'customer_contact_code':
        case 'vendor_contact_code':
        case 'all_contact_code':
            $sql = 'SELECT contact_code, contact_name, contact_type
                    FROM contacts
                    WHERE company_id = :company_id AND is_active = 1';
            if ($lookup === 'customer_contact_code') {
                $sql .= " AND contact_type IN ('customer', 'both')";
            } elseif ($lookup === 'vendor_contact_code') {
                $sql .= " AND contact_type IN ('vendor', 'both')";
            }
            $sql .= ' ORDER BY contact_code ASC';
            $stmt = $pdo->prepare($sql);
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'contact_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['contact_name'] ?? '')
            );

        case 'bank_account_name':
            $stmt = $pdo->prepare(
                'SELECT account_name, bank_name
                 FROM bank_accounts
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY account_name ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'account_name',
                static fn (array $row, string $value): string => $value . ((string) ($row['bank_name'] ?? '') !== '' ? ' - ' . (string) $row['bank_name'] : '')
            );

        case 'cash_account_name':
            $stmt = $pdo->prepare(
                'SELECT cash_name, custodian_name, location_name
                 FROM cash_accounts
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY cash_name ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'cash_name',
                static function (array $row, string $value): string {
                    $parts = [$value];
                    $custodian = app_normalize_whitespace((string) ($row['custodian_name'] ?? ''));
                    $location = app_normalize_whitespace((string) ($row['location_name'] ?? ''));
                    if ($custodian !== '') {
                        $parts[] = $custodian;
                    }
                    if ($location !== '') {
                        $parts[] = $location;
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'excellence_hospital_code':
            $stmt = $pdo->prepare(
                'SELECT hospital_code, hospital_name
                 FROM excellence_hospitals
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY hospital_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'hospital_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['hospital_name'] ?? '')
            );

        case 'excellence_device_code':
            $stmt = $pdo->prepare(
                'SELECT device_code, device_name, serial_number
                 FROM excellence_devices
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY device_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'device_code',
                static function (array $row, string $value): string {
                    $serial = app_normalize_whitespace((string) ($row['serial_number'] ?? ''));
                    return $value . ' - ' . (string) ($row['device_name'] ?? '') . ($serial !== '' ? ' - SN ' . $serial : '');
                }
            );

        case 'excellence_operation_number':
            $stmt = $pdo->prepare(
                "SELECT operation_number, operation_date, patient_name, procedure_name
                 FROM excellence_operations
                 WHERE company_id = :company_id AND status <> 'cancelled'
                 ORDER BY operation_date DESC, id DESC"
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'operation_number',
                static function (array $row, string $value): string {
                    $patient = app_normalize_whitespace((string) ($row['patient_name'] ?? ''));
                    $procedure = app_normalize_whitespace((string) ($row['procedure_name'] ?? ''));
                    $parts = [$value];
                    if ($patient !== '') {
                        $parts[] = $patient;
                    }
                    if ($procedure !== '') {
                        $parts[] = $procedure;
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'payment_account_name':
            $options = [];
            $cashStmt = $pdo->prepare(
                'SELECT cash_name, custodian_name, location_name
                 FROM cash_accounts
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY cash_name ASC'
            );
            $cashStmt->execute(['company_id' => $companyId]);
            foreach (($cashStmt->fetchAll() ?: []) as $row) {
                $name = app_normalize_whitespace((string) ($row['cash_name'] ?? ''));
                if ($name === '') {
                    continue;
                }
                $extra = app_normalize_whitespace((string) ($row['custodian_name'] ?? ''));
                if ($extra === '') {
                    $extra = app_normalize_whitespace((string) ($row['location_name'] ?? ''));
                }
                $options[$name] = 'Cash: ' . $name . ($extra !== '' ? ' - ' . $extra : '');
            }

            $bankStmt = $pdo->prepare(
                'SELECT account_name, bank_name
                 FROM bank_accounts
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY account_name ASC'
            );
            $bankStmt->execute(['company_id' => $companyId]);
            foreach (($bankStmt->fetchAll() ?: []) as $row) {
                $name = app_normalize_whitespace((string) ($row['account_name'] ?? ''));
                if ($name === '' || isset($options[$name])) {
                    continue;
                }
                $bankName = app_normalize_whitespace((string) ($row['bank_name'] ?? ''));
                $options[$name] = 'Bank: ' . $name . ($bankName !== '' ? ' - ' . $bankName : '');
            }
            return $options;

        case 'pos_register_id':
            $stmt = $pdo->prepare(
                'SELECT id, register_code, register_name
                 FROM pos_registers
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY register_code ASC, id ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $options = [];
            foreach (($stmt->fetchAll() ?: []) as $row) {
                $id = (int) ($row['id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $code = app_normalize_whitespace((string) ($row['register_code'] ?? ''));
                $name = app_normalize_whitespace((string) ($row['register_name'] ?? ''));
                $options[(string) $id] = trim($code . ($name !== '' ? ' - ' . $name : ''));
            }
            return $options;

        case 'product_sku':
            $stmt = $pdo->prepare(
                'SELECT sku, product_name
                 FROM products
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY sku ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'sku',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['product_name'] ?? '')
            );

        case 'asset_id':
            $stmt = $pdo->prepare(
                'SELECT id, asset_code, asset_name
                 FROM assets
                 WHERE company_id = :company_id AND status = :status
                 ORDER BY asset_code ASC, id ASC'
            );
            $stmt->execute([
                'company_id' => $companyId,
                'status' => 'active',
            ]);
            $rows = $stmt->fetchAll() ?: [];
            $options = [];
            foreach ($rows as $row) {
                $id = (int) ($row['id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $code = app_normalize_whitespace((string) ($row['asset_code'] ?? ''));
                $name = app_normalize_whitespace((string) ($row['asset_name'] ?? ''));
                $options[(string) $id] = trim($code . ($name !== '' ? ' - ' . $name : ''));
            }
            return $options;

        case 'sales_tax_name':
        case 'purchase_tax_name':
        case 'withholding_tax_name':
        case 'all_tax_name':
            $sql = 'SELECT tax_name, rate_percent, applies_to, tax_account_code
                    FROM tax_rates
                    WHERE company_id = :company_id
                      AND is_active = 1';
            if ($lookup === 'sales_tax_name') {
                $sql .= " AND applies_to IN ('sales', 'both')";
            } elseif ($lookup === 'purchase_tax_name') {
                $sql .= " AND applies_to IN ('purchase', 'both')";
            } elseif ($lookup === 'withholding_tax_name') {
                $sql .= " AND applies_to IN ('withholding', 'both')";
            }
            $sql .= ' ORDER BY rate_percent DESC, tax_name ASC';
            $stmt = $pdo->prepare($sql);
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'tax_name',
                static fn (array $row, string $value): string => $value . ' (' . app_money_format($row['rate_percent'] ?? 0) . '%)'
            );

        case 'cost_center_code':
            $stmt = $pdo->prepare(
                'SELECT center_code, center_name
                 FROM cost_centers
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY center_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'center_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['center_name'] ?? '')
            );

        case 'hr_department_code':
            $stmt = $pdo->prepare(
                'SELECT department_code, department_name
                 FROM hr_departments
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY department_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'department_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['department_name'] ?? '')
            );

        case 'hr_job_code':
            $stmt = $pdo->prepare(
                'SELECT job_code, job_title
                 FROM hr_job_titles
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY job_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'job_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['job_title'] ?? '')
            );

        case 'hr_shift_code':
            $stmt = $pdo->prepare(
                'SELECT shift_code, shift_name
                 FROM hr_shifts
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY shift_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'shift_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['shift_name'] ?? '')
            );

        case 'hr_leave_type_code':
            $stmt = $pdo->prepare(
                'SELECT leave_type_code, leave_type_name
                 FROM hr_leave_types
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY leave_type_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'leave_type_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['leave_type_name'] ?? '')
            );

        case 'hr_salary_component_code':
            $stmt = $pdo->prepare(
                'SELECT component_code, component_name, component_type
                 FROM hr_salary_components
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY component_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'component_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['component_name'] ?? '') . ' (' . (string) ($row['component_type'] ?? '') . ')'
            );

        case 'payroll_run_number':
            $stmt = $pdo->prepare(
                'SELECT run_number, period_month, period_year, status
                 FROM payroll_runs
                 WHERE company_id = :company_id
                 ORDER BY period_year DESC, period_month DESC, run_number DESC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'run_number',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['period_year'] ?? '') . '/' . (string) ($row['period_month'] ?? '') . ' (' . (string) ($row['status'] ?? '') . ')'
            );

        case 'employee_number':
            $stmt = $pdo->prepare(
                'SELECT employee_number, full_name
                 FROM employees
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY employee_number ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'employee_number',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['full_name'] ?? '')
            );

        case 'student_stage_code':
            $stmt = $pdo->prepare(
                'SELECT stage_code, stage_name
                 FROM student_stages
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY sort_order ASC, stage_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'stage_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['stage_name'] ?? '')
            );

        case 'student_grade_code':
            $stmt = $pdo->prepare(
                'SELECT grade_code, grade_name, stage_code
                 FROM student_grades
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY sort_order ASC, grade_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'grade_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['grade_name'] ?? '') . ' (' . (string) ($row['stage_code'] ?? '') . ')'
            );

        case 'student_class_code':
            $stmt = $pdo->prepare(
                'SELECT class_code, class_name, stage_code, grade_code
                 FROM student_classes
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY academic_year DESC, stage_code ASC, grade_code ASC, class_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'class_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['class_name'] ?? '') . ' (' . (string) ($row['grade_code'] ?? '') . ')'
            );

        case 'student_number':
            $stmt = $pdo->prepare(
                'SELECT student_number, student_name, grade_code, guardian_contact_code
                 FROM students
                 WHERE company_id = :company_id AND status = :status
                 ORDER BY student_number ASC'
            );
            $stmt->execute(['company_id' => $companyId, 'status' => 'active']);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'student_number',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['student_name'] ?? '') . ' (' . (string) ($row['grade_code'] ?? '') . ')'
            );

        case 'student_revenue_type_code':
            $stmt = $pdo->prepare(
                'SELECT revenue_type_code, revenue_type_name, default_amount
                 FROM student_revenue_types
                 WHERE company_id = :company_id AND is_active = 1
                 ORDER BY revenue_category ASC, revenue_type_code ASC'
            );
            $stmt->execute(['company_id' => $companyId]);
            $rows = $stmt->fetchAll() ?: [];
            return $build(
                $rows,
                'revenue_type_code',
                static fn (array $row, string $value): string => $value . ' - ' . (string) ($row['revenue_type_name'] ?? '') . ' (' . app_money_format($row['default_amount'] ?? 0) . ')'
            );

        case 'student_invoice_id':
            $rows = app_student_invoice_payment_candidates($companyId);
            return $build(
                $rows,
                'id',
                static function (array $row, string $value): string {
                    $invoiceNumber = app_normalize_whitespace((string) ($row['student_invoice_number'] ?? ('Invoice #' . $value)));
                    $student = app_normalize_whitespace((string) ($row['student_number'] ?? ''));
                    $remaining = app_student_invoice_remaining_amount($row);
                    $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                    $parts = [$invoiceNumber];
                    if ($student !== '') {
                        $parts[] = $student;
                    }
                    $parts[] = 'Remaining ' . app_money_format($remaining);
                    if ($status !== '') {
                        $parts[] = ucfirst(str_replace('_', ' ', $status));
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'sales_invoice_id':
            $rows = app_sales_invoice_payment_candidates($companyId);
            return $build(
                $rows,
                'id',
                static function (array $row, string $value): string {
                    $invoiceNumber = app_normalize_whitespace((string) ($row['invoice_number'] ?? ('Invoice #' . $value)));
                    $customer = app_normalize_whitespace((string) ($row['customer_code'] ?? ''));
                    $remaining = app_sales_invoice_remaining_amount($row);
                    $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                    $parts = [$invoiceNumber];
                    if ($customer !== '') {
                        $parts[] = $customer;
                    }
                    $parts[] = 'Remaining ' . app_money_format($remaining);
                    if ($status !== '') {
                        $parts[] = ucfirst(str_replace('_', ' ', $status));
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'purchase_bill_id':
            $rows = app_purchase_bill_payment_candidates($companyId);
            return $build(
                $rows,
                'id',
                static function (array $row, string $value): string {
                    $billNumber = app_normalize_whitespace((string) ($row['bill_number'] ?? ('Bill #' . $value)));
                    $vendor = app_normalize_whitespace((string) ($row['vendor_code'] ?? ''));
                    $remaining = app_purchase_bill_remaining_amount($row);
                    $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                    $parts = [$billNumber];
                    if ($vendor !== '') {
                        $parts[] = $vendor;
                    }
                    $parts[] = 'Remaining ' . app_money_format($remaining);
                    if ($status !== '') {
                        $parts[] = ucfirst(str_replace('_', ' ', $status));
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'expense_id':
            $rows = app_expense_payment_candidates($companyId);
            return $build(
                $rows,
                'id',
                static function (array $row, string $value): string {
                    $expenseNumber = app_normalize_whitespace((string) ($row['expense_number'] ?? ('Expense #' . $value)));
                    $payee = app_normalize_whitespace((string) ($row['payee_code'] ?? ''));
                    $remaining = app_expense_remaining_amount($row);
                    $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                    $parts = [$expenseNumber];
                    if ($payee !== '') {
                        $parts[] = $payee;
                    }
                    $parts[] = 'Remaining ' . app_money_format($remaining);
                    if ($status !== '') {
                        $parts[] = ucfirst(str_replace('_', ' ', $status));
                    }
                    return implode(' - ', $parts);
                }
            );

        case 'prepaid_expense_id':
            $rows = app_prepaid_expense_amortization_candidates($companyId);
            return $build(
                $rows,
                'id',
                static function (array $row, string $value): string {
                    $prepaidNumber = app_normalize_whitespace((string) ($row['prepaid_number'] ?? ('Prepaid #' . $value)));
                    $description = app_normalize_whitespace((string) ($row['description'] ?? ''));
                    $remaining = max(0, app_number($row['total_amount'] ?? 0) - app_number($row['amortized_amount'] ?? 0));
                    $parts = [$prepaidNumber];
                    if ($description !== '') {
                        $parts[] = $description;
                    }
                    $parts[] = 'Remaining ' . app_money_format($remaining);
                    return implode(' - ', $parts);
                }
            );

        case 'ifrs16_lease_id':
            $stmt = $pdo->prepare(
                "SELECT id, lease_number, lease_name, status
                 FROM ifrs16_leases
                 WHERE company_id = :company_id
                   AND status <> 'cancelled'
                 ORDER BY commencement_date DESC, id DESC"
            );
            $stmt->execute(['company_id' => $companyId]);
            $options = [];
            foreach (($stmt->fetchAll() ?: []) as $row) {
                $id = (int) ($row['id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $leaseNumber = app_normalize_whitespace((string) ($row['lease_number'] ?? ('Lease #' . $id)));
                $leaseName = app_normalize_whitespace((string) ($row['lease_name'] ?? ''));
                $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                $parts = [$leaseNumber];
                if ($leaseName !== '') {
                    $parts[] = $leaseName;
                }
                if ($status !== '') {
                    $parts[] = ucfirst(str_replace('_', ' ', $status));
                }
                $options[(string) $id] = implode(' - ', $parts);
            }
            return $options;

        case 'ifrs16_schedule_id':
            $stmt = $pdo->prepare(
                "SELECT s.id, s.lease_id, s.lease_number, s.sequence_no, s.schedule_date, s.payment_amount, s.status,
                        l.lease_name
                 FROM ifrs16_lease_schedules s
                 INNER JOIN ifrs16_leases l
                         ON l.company_id = s.company_id
                        AND l.id = s.lease_id
                 WHERE s.company_id = :company_id
                   AND l.status NOT IN ('draft', 'cancelled')
                 ORDER BY s.schedule_date ASC, s.sequence_no ASC, s.id ASC"
            );
            $stmt->execute(['company_id' => $companyId]);
            $options = [];
            foreach (($stmt->fetchAll() ?: []) as $row) {
                $id = (int) ($row['id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $leaseNumber = app_normalize_whitespace((string) ($row['lease_number'] ?? ('Lease #' . (int) ($row['lease_id'] ?? 0))));
                $sequence = app_integer($row['sequence_no'] ?? 0);
                $date = app_normalize_whitespace((string) ($row['schedule_date'] ?? ''));
                $status = app_normalize_whitespace((string) ($row['status'] ?? ''));
                $parts = [$leaseNumber];
                if ($sequence > 0) {
                    $parts[] = '#' . str_pad((string) $sequence, 3, '0', STR_PAD_LEFT);
                }
                if ($date !== '') {
                    $parts[] = $date;
                }
                $parts[] = 'Payment ' . app_money_format($row['payment_amount'] ?? 0);
                $parts[] = 'Remaining ' . app_money_format(app_ifrs16_schedule_remaining_amount($companyId, $row));
                if ($status !== '') {
                    $parts[] = ucfirst(str_replace('_', ' ', $status));
                }
                $options[(string) $id] = implode(' - ', $parts);
            }
            return $options;
    }

    return [];
}

function app_account_exists(int $companyId, string $accountCode, ?string $accountType = null): bool
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        return false;
    }

    $pdo = app_pdo();
    $sql = 'SELECT 1 FROM chart_of_accounts WHERE company_id = :company_id AND account_code = :account_code';
    $params = [
        'company_id' => $companyId,
        'account_code' => $accountCode,
    ];
    if ($accountType !== null && $accountType !== '') {
        $sql .= ' AND account_type = :account_type';
        $params['account_type'] = $accountType;
    }
    $sql .= ' LIMIT 1';
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return (bool) $stmt->fetchColumn();
}

function app_account_type_by_code(int $companyId, string $accountCode): ?string
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT account_type
         FROM chart_of_accounts
         WHERE company_id = :company_id
           AND account_code = :account_code
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'account_code' => $accountCode,
    ]);
    $type = app_normalize_whitespace((string) ($stmt->fetchColumn() ?: ''));
    return $type !== '' ? strtolower($type) : null;
}

function app_cost_center_exists(int $companyId, string $centerCode, bool $onlyActive = true): bool
{
    $centerCode = app_normalize_whitespace($centerCode);
    if ($companyId <= 0 || $centerCode === '') {
        return false;
    }

    $sql = 'SELECT 1
            FROM cost_centers
            WHERE company_id = :company_id
              AND center_code = :center_code';
    $params = [
        'company_id' => $companyId,
        'center_code' => $centerCode,
    ];
    if ($onlyActive) {
        $sql .= ' AND is_active = 1';
    }
    $sql .= ' LIMIT 1';

    $stmt = app_pdo()->prepare($sql);
    $stmt->execute($params);
    return (bool) $stmt->fetchColumn();
}

function app_account_code_by_type(int $companyId, string $accountType, array $preferredCodes = []): ?string
{
    $accounts = app_chart_accounts($companyId, $accountType, true);
    $accounts = array_values(array_filter(
        $accounts,
        static fn (array $account): bool => !app_account_has_children($companyId, (string) ($account['account_code'] ?? ''))
    ));
    if (!$accounts) {
        return null;
    }

    $available = [];
    foreach ($accounts as $account) {
        $code = app_normalize_whitespace((string) ($account['account_code'] ?? ''));
        if ($code !== '') {
            $available[$code] = true;
        }
    }

    foreach ($preferredCodes as $preferred) {
        $preferred = app_normalize_whitespace((string) $preferred);
        if ($preferred !== '' && isset($available[$preferred])) {
            return $preferred;
        }
    }

    return app_normalize_whitespace((string) ($accounts[0]['account_code'] ?? '')) ?: null;
}

function app_resolve_account_code(
    int $companyId,
    string $requestedCode,
    string $expectedType,
    array $preferredFallbacks
): string {
    $requestedCode = app_normalize_whitespace($requestedCode);
    if ($requestedCode !== '') {
        app_assert_leaf_account($companyId, $requestedCode, $expectedType, ucfirst($expectedType) . ' account');
        return $requestedCode;
    }

    $resolved = app_account_code_by_type($companyId, $expectedType, $preferredFallbacks);
    if ($resolved === null || $resolved === '') {
        throw new RuntimeException('Missing account of type "' . $expectedType . '" in chart of accounts.');
    }
    return $resolved;
}

function app_posting_account_settings(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT sales_receivable_account_code,
                purchase_payable_account_code,
                accrued_expense_account_code,
                prepaid_expense_account_code,
                ifrs16_rou_asset_account_code,
                ifrs16_lease_liability_account_code,
                ifrs16_interest_expense_account_code,
                ifrs16_depreciation_expense_account_code,
                ifrs16_accumulated_depreciation_account_code,
                ifrs16_payment_account_code,
                ifrs16_restoration_provision_account_code,
                ifrs16_lease_incentive_account_code,
                pos_sales_revenue_account_code,
                pos_cash_over_short_gain_account_code,
                pos_cash_over_short_loss_account_code
         FROM posting_account_settings
         WHERE company_id = :company_id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId]);
    $row = $stmt->fetch();
    if ($row) {
        return $row;
    }

    $insertStmt = $pdo->prepare(
        'INSERT IGNORE INTO posting_account_settings
         (company_id, sales_receivable_account_code, purchase_payable_account_code, accrued_expense_account_code, prepaid_expense_account_code,
          ifrs16_rou_asset_account_code, ifrs16_lease_liability_account_code, ifrs16_interest_expense_account_code,
          ifrs16_depreciation_expense_account_code, ifrs16_accumulated_depreciation_account_code, ifrs16_payment_account_code,
          ifrs16_restoration_provision_account_code, ifrs16_lease_incentive_account_code,
          pos_sales_revenue_account_code, pos_cash_over_short_gain_account_code, pos_cash_over_short_loss_account_code)
         VALUES
         (:company_id, :sales_receivable_account_code, :purchase_payable_account_code, :accrued_expense_account_code, :prepaid_expense_account_code,
          :ifrs16_rou_asset_account_code, :ifrs16_lease_liability_account_code, :ifrs16_interest_expense_account_code,
          :ifrs16_depreciation_expense_account_code, :ifrs16_accumulated_depreciation_account_code, :ifrs16_payment_account_code,
          :ifrs16_restoration_provision_account_code, :ifrs16_lease_incentive_account_code,
          :pos_sales_revenue_account_code, :pos_cash_over_short_gain_account_code, :pos_cash_over_short_loss_account_code)'
    );
    $insertStmt->execute([
        'company_id' => $companyId,
        'sales_receivable_account_code' => '1121',
        'purchase_payable_account_code' => '2111',
        'accrued_expense_account_code' => '2130',
        'prepaid_expense_account_code' => '1150',
        'ifrs16_rou_asset_account_code' => '1231',
        'ifrs16_lease_liability_account_code' => '2210',
        'ifrs16_interest_expense_account_code' => '5700',
        'ifrs16_depreciation_expense_account_code' => '5600',
        'ifrs16_accumulated_depreciation_account_code' => '1239',
        'ifrs16_payment_account_code' => '1112',
        'ifrs16_restoration_provision_account_code' => '2220',
        'ifrs16_lease_incentive_account_code' => '1160',
        'pos_sales_revenue_account_code' => '4100',
        'pos_cash_over_short_gain_account_code' => '4100',
        'pos_cash_over_short_loss_account_code' => '5510',
    ]);

    return [
        'sales_receivable_account_code' => '1121',
        'purchase_payable_account_code' => '2111',
        'accrued_expense_account_code' => '2130',
        'prepaid_expense_account_code' => '1150',
        'ifrs16_rou_asset_account_code' => '1231',
        'ifrs16_lease_liability_account_code' => '2210',
        'ifrs16_interest_expense_account_code' => '5700',
        'ifrs16_depreciation_expense_account_code' => '5600',
        'ifrs16_accumulated_depreciation_account_code' => '1239',
        'ifrs16_payment_account_code' => '1112',
        'ifrs16_restoration_provision_account_code' => '2220',
        'ifrs16_lease_incentive_account_code' => '1160',
        'pos_sales_revenue_account_code' => '4100',
        'pos_cash_over_short_gain_account_code' => '4100',
        'pos_cash_over_short_loss_account_code' => '5510',
    ];
}

function app_posting_account_code(int $companyId, string $settingKey, string $expectedType, array $preferredFallbacks): string
{
    $settings = app_posting_account_settings($companyId);
    $field = match ($settingKey) {
        'sales_receivable' => 'sales_receivable_account_code',
        'purchase_payable' => 'purchase_payable_account_code',
        'accrued_expense' => 'accrued_expense_account_code',
        'prepaid_expense' => 'prepaid_expense_account_code',
        'ifrs16_rou_asset' => 'ifrs16_rou_asset_account_code',
        'ifrs16_lease_liability' => 'ifrs16_lease_liability_account_code',
        'ifrs16_interest_expense' => 'ifrs16_interest_expense_account_code',
        'ifrs16_depreciation_expense' => 'ifrs16_depreciation_expense_account_code',
        'ifrs16_accumulated_depreciation' => 'ifrs16_accumulated_depreciation_account_code',
        'ifrs16_payment' => 'ifrs16_payment_account_code',
        'ifrs16_restoration_provision' => 'ifrs16_restoration_provision_account_code',
        'ifrs16_lease_incentive' => 'ifrs16_lease_incentive_account_code',
        'pos_sales_revenue' => 'pos_sales_revenue_account_code',
        'pos_cash_over_short_gain' => 'pos_cash_over_short_gain_account_code',
        'pos_cash_over_short_loss' => 'pos_cash_over_short_loss_account_code',
        default => '',
    };

    $requestedCode = $field !== '' ? app_normalize_whitespace((string) ($settings[$field] ?? '')) : '';
    return app_resolve_account_code($companyId, $requestedCode, $expectedType, $preferredFallbacks);
}

function app_resolve_bank_gl_account_code(int $companyId, string $bankAccountName): ?string
{
    $bankAccountName = app_normalize_whitespace($bankAccountName);
    if ($companyId <= 0 || $bankAccountName === '') {
        return null;
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT gl_account_code
         FROM bank_accounts
         WHERE company_id = :company_id
           AND account_name = :account_name
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'account_name' => $bankAccountName,
    ]);
    $code = app_normalize_whitespace((string) ($stmt->fetchColumn() ?: ''));
    return $code !== '' ? $code : null;
}

function app_resolve_cash_gl_account_code(int $companyId, string $cashAccountName): ?string
{
    $cashAccountName = app_normalize_whitespace($cashAccountName);
    if ($companyId <= 0 || $cashAccountName === '') {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT gl_account_code
         FROM cash_accounts
         WHERE company_id = :company_id
           AND cash_name = :cash_name
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'cash_name' => $cashAccountName,
    ]);
    $code = app_normalize_whitespace((string) ($stmt->fetchColumn() ?: ''));
    return $code !== '' ? $code : null;
}

function app_payment_account_category_for_method(string $paymentMethod): string
{
    $paymentMethod = strtolower(app_normalize_whitespace($paymentMethod));
    return $paymentMethod === 'cash' ? 'cash' : (in_array($paymentMethod, ['bank', 'card', 'transfer'], true) ? 'bank' : '');
}

function app_payment_account_category_map(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $map = [];
    $pdo = app_pdo();

    $cashStmt = $pdo->prepare(
        'SELECT cash_name
         FROM cash_accounts
         WHERE company_id = :company_id AND is_active = 1
         ORDER BY cash_name ASC'
    );
    $cashStmt->execute(['company_id' => $companyId]);
    foreach (($cashStmt->fetchAll() ?: []) as $row) {
        $name = app_normalize_whitespace((string) ($row['cash_name'] ?? ''));
        if ($name !== '') {
            $map[$name] = 'cash';
        }
    }

    $bankStmt = $pdo->prepare(
        'SELECT account_name
         FROM bank_accounts
         WHERE company_id = :company_id AND is_active = 1
         ORDER BY account_name ASC'
    );
    $bankStmt->execute(['company_id' => $companyId]);
    foreach (($bankStmt->fetchAll() ?: []) as $row) {
        $name = app_normalize_whitespace((string) ($row['account_name'] ?? ''));
        if ($name !== '' && !isset($map[$name])) {
            $map[$name] = 'bank';
        }
    }

    return $map;
}

function app_cash_account_exists(int $companyId, string $cashAccountName): bool
{
    $cashAccountName = app_normalize_whitespace($cashAccountName);
    if ($companyId <= 0 || $cashAccountName === '') {
        return false;
    }

    $stmt = app_pdo()->prepare(
        'SELECT 1 FROM cash_accounts WHERE company_id = :company_id AND cash_name = :cash_name LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'cash_name' => $cashAccountName,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_payment_account_exists(int $companyId, string $accountName, string $paymentMethod = ''): bool
{
    $accountName = app_normalize_whitespace($accountName);
    if ($companyId <= 0 || $accountName === '') {
        return false;
    }

    $category = app_payment_account_category_for_method($paymentMethod);
    if ($category === 'cash') {
        return app_cash_account_exists($companyId, $accountName);
    }
    if ($category === 'bank') {
        return app_bank_account_exists($companyId, $accountName);
    }

    return app_cash_account_exists($companyId, $accountName) || app_bank_account_exists($companyId, $accountName);
}

function app_resolve_payment_account_gl_account_code(int $companyId, string $accountName, string $paymentMethod = ''): ?string
{
    $accountName = app_normalize_whitespace($accountName);
    if ($companyId <= 0 || $accountName === '') {
        return null;
    }

    $category = app_payment_account_category_for_method($paymentMethod);
    if ($category === 'cash') {
        return app_resolve_cash_gl_account_code($companyId, $accountName);
    }
    if ($category === 'bank') {
        return app_resolve_bank_gl_account_code($companyId, $accountName);
    }

    return app_resolve_cash_gl_account_code($companyId, $accountName)
        ?? app_resolve_bank_gl_account_code($companyId, $accountName);
}

function app_assert_payment_account_postable(int $companyId, string $accountName, string $label = 'Cash/bank account', string $paymentMethod = ''): void
{
    $accountName = app_normalize_whitespace($accountName);
    if ($companyId <= 0 || $accountName === '') {
        throw new RuntimeException($label . ' is required.');
    }
    if (!app_payment_account_exists($companyId, $accountName, $paymentMethod)) {
        throw new RuntimeException('Select a valid ' . strtolower($label) . '.');
    }

    $glAccountCode = app_resolve_payment_account_gl_account_code($companyId, $accountName, $paymentMethod);
    if ($glAccountCode !== null && $glAccountCode !== '') {
        app_assert_leaf_account($companyId, $glAccountCode, 'asset', $label . ' GL account');
    }
}

function app_assert_bank_account_postable(int $companyId, string $bankAccountName, string $label = 'Cash/bank account'): void
{
    app_assert_payment_account_postable($companyId, $bankAccountName, $label);
}

function app_contact_exists(int $companyId, string $contactCode, ?string $type = null): bool
{
    $contactCode = app_normalize_whitespace($contactCode);
    if ($companyId <= 0 || $contactCode === '') {
        return false;
    }

    $pdo = app_pdo();
    $sql = 'SELECT 1 FROM contacts WHERE company_id = :company_id AND contact_code = :contact_code';
    $params = [
        'company_id' => $companyId,
        'contact_code' => $contactCode,
    ];

    if ($type === 'customer') {
        $sql .= " AND contact_type IN ('customer', 'both')";
    } elseif ($type === 'vendor') {
        $sql .= " AND contact_type IN ('vendor', 'both')";
    }
    $sql .= ' LIMIT 1';
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return (bool) $stmt->fetchColumn();
}

function app_contact_row(int $companyId, string $contactCode, ?string $type = null): ?array
{
    $contactCode = app_normalize_whitespace($contactCode);
    if ($companyId <= 0 || $contactCode === '') {
        return null;
    }

    $sql = 'SELECT *
            FROM contacts
            WHERE company_id = :company_id AND contact_code = :contact_code';
    $params = [
        'company_id' => $companyId,
        'contact_code' => $contactCode,
    ];

    if ($type === 'customer') {
        $sql .= " AND contact_type IN ('customer', 'both')";
    } elseif ($type === 'vendor') {
        $sql .= " AND contact_type IN ('vendor', 'both')";
    }
    $sql .= ' LIMIT 1';

    $stmt = app_pdo()->prepare($sql);
    $stmt->execute($params);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_bank_account_exists(int $companyId, string $accountName): bool
{
    $accountName = app_normalize_whitespace($accountName);
    if ($companyId <= 0 || $accountName === '') {
        return false;
    }

    $stmt = app_pdo()->prepare(
        'SELECT 1 FROM bank_accounts WHERE company_id = :company_id AND account_name = :account_name LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'account_name' => $accountName,
    ]);
    return (bool) $stmt->fetchColumn();
}

function app_tax_rate_row_by_name(int $companyId, string $taxName, string $scope): ?array
{
    $taxName = app_normalize_whitespace($taxName);
    $scope = in_array($scope, ['sales', 'purchase', 'withholding'], true) ? $scope : 'sales';
    if ($companyId <= 0 || $taxName === '') {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT tax_name, rate_percent, applies_to, tax_account_code
         FROM tax_rates
         WHERE company_id = :company_id
           AND tax_name = :tax_name
           AND is_active = 1
           AND applies_to IN (:scope, :both)
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'tax_name' => $taxName,
        'scope' => $scope,
        'both' => 'both',
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_default_tax_rate_row(int $companyId, string $scope): ?array
{
    $scope = in_array($scope, ['sales', 'purchase', 'withholding'], true) ? $scope : 'sales';
    if ($companyId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT tax_name, rate_percent, applies_to, tax_account_code
         FROM tax_rates
         WHERE company_id = :company_id
           AND is_active = 1
           AND applies_to IN (:scope, :both)
         ORDER BY rate_percent DESC, id ASC
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'scope' => $scope,
        'both' => 'both',
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_tax_rate_map(int $companyId, string $scope): array
{
    $scope = in_array($scope, ['sales', 'purchase', 'withholding'], true) ? $scope : 'both';
    if ($companyId <= 0) {
        return [];
    }

    $sql = 'SELECT tax_name, rate_percent
            FROM tax_rates
            WHERE company_id = :company_id
              AND is_active = 1';
    if ($scope === 'sales') {
        $sql .= " AND applies_to IN ('sales', 'both')";
    } elseif ($scope === 'purchase') {
        $sql .= " AND applies_to IN ('purchase', 'both')";
    } elseif ($scope === 'withholding') {
        $sql .= " AND applies_to IN ('withholding', 'both')";
    }
    $sql .= ' ORDER BY rate_percent DESC, tax_name ASC';

    $stmt = app_pdo()->prepare($sql);
    $stmt->execute(['company_id' => $companyId]);
    $map = [];
    foreach (($stmt->fetchAll() ?: []) as $row) {
        $name = app_normalize_whitespace((string) ($row['tax_name'] ?? ''));
        if ($name !== '') {
            $map[$name] = app_number($row['rate_percent'] ?? 0);
        }
    }
    return $map;
}

function app_tax_posting_account_code(int $companyId, string $taxName, string $scope): ?string
{
    $row = app_tax_rate_row_by_name($companyId, $taxName, $scope);
    $accountCode = app_normalize_whitespace((string) ($row['tax_account_code'] ?? ''));
    if ($accountCode === '') {
        return null;
    }

    app_assert_leaf_account($companyId, $accountCode, null, 'Tax posting account');
    return $accountCode;
}

function app_document_payment_status(string $slug, float $grandTotal, float $paidAmount, string $currentStatus = ''): string
{
    $currentStatus = strtolower(app_normalize_whitespace($currentStatus));
    if (in_array($currentStatus, ['draft', 'cancelled'], true)) {
        return $currentStatus;
    }

    $defaultOpenStatus = in_array($slug, ['sales_invoices', 'sales_returns', 'student_invoices'], true) ? 'issued' : 'approved';
    if ($grandTotal <= 0 || $paidAmount <= 0) {
        return $defaultOpenStatus;
    }
    if ($paidAmount >= ($grandTotal - 0.01)) {
        return 'paid';
    }
    return 'partially_paid';
}

function app_sales_invoice_remaining_amount(array $invoice): float
{
    $grandTotal = max(0, app_number($invoice['grand_total'] ?? 0));
    $paidAmount = max(0, app_number($invoice['paid_amount'] ?? 0));
    return round(max(0, $grandTotal - $paidAmount), 2);
}

function app_sales_invoice_by_id(int $companyId, int $invoiceId): ?array
{
    if ($companyId <= 0 || $invoiceId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT id, invoice_number, invoice_date, customer_code, grand_total, paid_amount, inline_paid_amount, status
         FROM sales_invoices
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'id' => $invoiceId,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_sales_invoice_payment_candidates(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        "SELECT id, invoice_number, invoice_date, customer_code, grand_total, paid_amount, status
         FROM sales_invoices
         WHERE company_id = :company_id
           AND status NOT IN ('draft', 'cancelled')
         ORDER BY invoice_date DESC, id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_purchase_bill_remaining_amount(array $bill): float
{
    $grandTotal = max(0, app_number($bill['grand_total'] ?? 0));
    $withholdingTaxTotal = max(0, app_number($bill['withholding_tax_total'] ?? 0));
    $netPayable = max(0, app_number($bill['net_payable'] ?? 0));
    if ($netPayable <= 0 && $grandTotal > 0) {
        $netPayable = max(0, $grandTotal - $withholdingTaxTotal);
    }
    $paymentBasisTotal = $netPayable > 0 ? $netPayable : $grandTotal;
    $paidAmount = max(0, app_number($bill['paid_amount'] ?? 0));
    return round(max(0, $paymentBasisTotal - $paidAmount), 2);
}

function app_purchase_bill_by_id(int $companyId, int $billId): ?array
{
    if ($companyId <= 0 || $billId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT id, bill_number, bill_date, vendor_code, grand_total, withholding_tax_total, net_payable, paid_amount, inline_paid_amount, status
         FROM purchase_bills
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'id' => $billId,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_purchase_bill_payment_candidates(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        "SELECT id, bill_number, bill_date, vendor_code, grand_total, withholding_tax_total, net_payable, paid_amount, status
         FROM purchase_bills
         WHERE company_id = :company_id
           AND status NOT IN ('draft', 'cancelled')
         ORDER BY bill_date DESC, id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_expense_remaining_amount(array $expense): float
{
    $grandTotal = max(0, app_number($expense['grand_total'] ?? 0));
    $paidAmount = max(0, app_number($expense['paid_amount'] ?? 0));
    return round(max(0, $grandTotal - $paidAmount), 2);
}

function app_expense_by_id(int $companyId, int $expenseId): ?array
{
    if ($companyId <= 0 || $expenseId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT id, expense_number, expense_date, payee_code, offset_account_code, grand_total, paid_amount, inline_paid_amount, status
         FROM expenses
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'id' => $expenseId,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_expense_payment_candidates(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        "SELECT id, expense_number, expense_date, payee_code, offset_account_code, grand_total, paid_amount, status
         FROM expenses
         WHERE company_id = :company_id
           AND status NOT IN ('draft', 'cancelled')
         ORDER BY expense_date DESC, id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_normalize_invoice_bill_payload(string $slug, array $data, ?int $companyId): array
{
    if (!in_array($slug, ['sales_invoices', 'purchase_bills', 'expenses', 'sales_returns', 'purchase_returns', 'expense_returns'], true)) {
        return $data;
    }

    if ($slug === 'sales_returns') {
        $originalId = app_integer($data['original_invoice_id'] ?? 0);
        $data['original_invoice_id'] = $originalId > 0 ? $originalId : null;
    } elseif ($slug === 'purchase_returns') {
        $originalId = app_integer($data['original_bill_id'] ?? 0);
        $data['original_bill_id'] = $originalId > 0 ? $originalId : null;
    } elseif ($slug === 'expense_returns') {
        $originalId = app_integer($data['original_expense_id'] ?? 0);
        $data['original_expense_id'] = $originalId > 0 ? $originalId : null;
    }

    $subtotal = max(0, app_number($data['subtotal'] ?? 0));
    $discount = max(0, app_number($data['discount_total'] ?? 0));
    $baseAmount = max(0, $subtotal - $discount);

    $taxScope = in_array($slug, ['sales_invoices', 'sales_returns'], true) ? 'sales' : 'purchase';
    $isTaxable = !empty($data['is_taxable']) ? 1 : 0;
    $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
    $taxRate = max(0, app_number($data['tax_rate_percent'] ?? 0));
    if ($isTaxable === 1 && $companyId !== null && $companyId > 0) {
        if ($taxName !== '') {
            $row = app_tax_rate_row_by_name($companyId, $taxName, $taxScope);
            if ($row) {
                $taxRate = max(0, app_number($row['rate_percent'] ?? $taxRate));
            }
        } else {
            $defaultRate = app_default_tax_rate_row($companyId, $taxScope);
            if ($defaultRate) {
                $taxName = app_normalize_whitespace((string) ($defaultRate['tax_name'] ?? ''));
                $taxRate = max(0, app_number($defaultRate['rate_percent'] ?? $taxRate));
            }
        }
    }
    if ($isTaxable === 1 && $taxRate <= 0) {
        $taxRate = 15;
    }
    if ($isTaxable === 0) {
        $taxRate = 0;
        $taxName = '';
    }
    $taxTotal = $isTaxable === 1 ? round($baseAmount * ($taxRate / 100), 2) : 0.0;
    $grandTotal = round($baseAmount + $taxTotal, 2);
    $statusTotal = $grandTotal;

    if ($slug === 'purchase_bills') {
        $withholdingTaxName = app_normalize_whitespace((string) ($data['withholding_tax_name'] ?? ''));
        $withholdingTaxRate = max(0, app_number($data['withholding_tax_rate_percent'] ?? 0));

        if ($companyId !== null && $companyId > 0) {
            $vendorCode = app_normalize_whitespace((string) ($data['vendor_code'] ?? ''));
            if ($withholdingTaxName === '' && $vendorCode !== '') {
                $vendor = app_contact_row($companyId, $vendorCode, 'vendor');
                if ($vendor) {
                    $withholdingTaxName = app_normalize_whitespace((string) ($vendor['withholding_tax_name'] ?? ''));
                    $withholdingTaxRate = max(0, app_number($vendor['withholding_tax_rate_percent'] ?? $withholdingTaxRate));
                }
            }
            if ($withholdingTaxName !== '') {
                $withholdingTax = app_tax_rate_row_by_name($companyId, $withholdingTaxName, 'withholding');
                if ($withholdingTax) {
                    $withholdingTaxRate = max(0, app_number($withholdingTax['rate_percent'] ?? $withholdingTaxRate));
                }
            }
        }

        if ($withholdingTaxName === '') {
            $withholdingTaxRate = 0;
        }

        $withholdingTaxTotal = round($baseAmount * ($withholdingTaxRate / 100), 2);
        if ($withholdingTaxTotal > $grandTotal) {
            $withholdingTaxTotal = $grandTotal;
        }
        $netPayable = round(max(0, $grandTotal - $withholdingTaxTotal), 2);

        $data['withholding_tax_name'] = $withholdingTaxName;
        $data['withholding_tax_rate_percent'] = $withholdingTaxRate;
        $data['withholding_tax_total'] = $withholdingTaxTotal;
        $data['net_payable'] = $netPayable;
        $statusTotal = $netPayable;
    }

    $inlinePaidAmount = max(0, app_number($data['inline_paid_amount'] ?? ($data['paid_amount'] ?? 0)));
    if ($inlinePaidAmount > $statusTotal) {
        $inlinePaidAmount = $statusTotal;
    }
    if ($inlinePaidAmount <= 0) {
        $inlinePaidAmount = 0;
    }

    $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
    $paymentAccountName = app_normalize_whitespace((string) ($data['payment_account_name'] ?? ''));
    if ($inlinePaidAmount <= 0) {
        $paymentMethod = '';
        $paymentAccountName = '';
    }

    $data['subtotal'] = $subtotal;
    $data['discount_total'] = $discount;
    $data['is_taxable'] = $isTaxable;
    $data['tax_rate_percent'] = $taxRate;
    $data['tax_name'] = $taxName;
    $data['tax_total'] = $taxTotal;
    $data['grand_total'] = $grandTotal;
    $data['inline_paid_amount'] = $inlinePaidAmount;
    $data['paid_amount'] = $inlinePaidAmount;
    $data['payment_method'] = $paymentMethod;
    $data['payment_account_name'] = $paymentAccountName;
    $data['status'] = app_document_payment_status($slug, $statusTotal, $inlinePaidAmount, (string) ($data['status'] ?? ''));
    if (in_array($slug, ['expenses', 'expense_returns'], true)) {
        $remainingAmount = max(0, round($grandTotal - $inlinePaidAmount, 2));
        $offsetAccountCode = app_normalize_whitespace((string) ($data['offset_account_code'] ?? ''));
        $data['offset_account_code'] = $remainingAmount > 0.01 ? $offsetAccountCode : '';
    }

    return $data;
}

function app_normalize_payment_payload(array $data, ?int $companyId): array
{
    $currentPaymentId = app_integer($data['__record_id'] ?? 0);
    $salesInvoiceId = app_integer($data['sales_invoice_id'] ?? 0);
    $data['sales_invoice_id'] = $salesInvoiceId > 0 ? $salesInvoiceId : null;
    $billId = app_integer($data['purchase_bill_id'] ?? 0);
    $data['purchase_bill_id'] = $billId > 0 ? $billId : null;
    $expenseId = app_integer($data['expense_id'] ?? 0);
    $data['expense_id'] = $expenseId > 0 ? $expenseId : null;
    $ifrs16LeaseId = app_integer($data['ifrs16_lease_id'] ?? 0);
    $submittedIfrs16LeaseId = $ifrs16LeaseId;
    $data['ifrs16_lease_id'] = $ifrs16LeaseId > 0 ? $ifrs16LeaseId : null;
    $ifrs16ScheduleId = app_integer($data['ifrs16_schedule_id'] ?? 0);
    $data['ifrs16_schedule_id'] = $ifrs16ScheduleId > 0 ? $ifrs16ScheduleId : null;

    if (($salesInvoiceId <= 0 && $billId <= 0 && $expenseId <= 0 && $ifrs16LeaseId <= 0 && $ifrs16ScheduleId <= 0) || $companyId === null || $companyId <= 0) {
        return $data;
    }

    if ($ifrs16ScheduleId > 0 || $ifrs16LeaseId > 0) {
        if ($submittedIfrs16LeaseId > 0) {
            $data['__submitted_ifrs16_lease_id'] = $submittedIfrs16LeaseId;
        }
        $lease = null;
        $schedule = null;
        if ($ifrs16ScheduleId > 0) {
            $schedule = app_ifrs16_schedule_by_id($companyId, $ifrs16ScheduleId);
            if ($schedule) {
                $ifrs16LeaseId = app_integer($schedule['lease_id'] ?? $ifrs16LeaseId);
                $data['ifrs16_lease_id'] = $ifrs16LeaseId > 0 ? $ifrs16LeaseId : null;
            }
        }
        if ($ifrs16LeaseId > 0) {
            $lease = app_ifrs16_lease_by_id($companyId, $ifrs16LeaseId);
        }
        if (!$lease) {
            return $data;
        }

        $data['sales_invoice_id'] = null;
        $data['purchase_bill_id'] = null;
        $data['expense_id'] = null;
        $data['direction'] = 'out';

        $leaseNumber = app_normalize_whitespace((string) ($lease['lease_number'] ?? ''));
        $referenceParts = [$leaseNumber !== '' ? $leaseNumber : ('IFRS16 Lease #' . $ifrs16LeaseId)];
        if ($schedule) {
            $sequence = app_integer($schedule['sequence_no'] ?? 0);
            if ($sequence > 0) {
                $referenceParts[] = '#' . str_pad((string) $sequence, 3, '0', STR_PAD_LEFT);
            }
            $scheduleDate = app_normalize_whitespace((string) ($schedule['schedule_date'] ?? ''));
            if ($scheduleDate !== '') {
                $referenceParts[] = $scheduleDate;
            }
        }
        $data['reference'] = implode(' - ', $referenceParts);
        $data['contact_code'] = app_normalize_whitespace((string) ($lease['lessor_code'] ?? ''));

        $amount = max(0, app_number($data['amount'] ?? 0));
        $remaining = $schedule
            ? app_ifrs16_schedule_remaining_amount($companyId, $schedule, $currentPaymentId > 0 ? $currentPaymentId : null)
            : 0.0;
        if ($amount <= 0 && $remaining > 0) {
            $data['amount'] = $remaining;
        }
        $data['offset_account_code'] = app_resolve_account_code(
            $companyId,
            (string) ($lease['lease_liability_account_code'] ?? ''),
            'liability',
            ['2210', '2200', '2130']
        );

        return $data;
    }

    if ($salesInvoiceId > 0) {
        $invoice = app_sales_invoice_by_id($companyId, $salesInvoiceId);
        if (!$invoice) {
            return $data;
        }

        $data['purchase_bill_id'] = null;
        $data['expense_id'] = null;
        $data['ifrs16_lease_id'] = null;
        $data['ifrs16_schedule_id'] = null;
        $data['direction'] = 'in';
        $data['reference'] = app_normalize_whitespace((string) ($invoice['invoice_number'] ?? ''));
        $data['contact_code'] = app_normalize_whitespace((string) ($invoice['customer_code'] ?? ''));

        $amount = max(0, app_number($data['amount'] ?? 0));
        $remaining = app_sales_invoice_remaining_amount($invoice);
        if ($amount <= 0 && $remaining > 0) {
            $data['amount'] = $remaining;
        }

        $data['offset_account_code'] = app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);

        return $data;
    }

    if ($billId > 0) {
        $bill = app_purchase_bill_by_id($companyId, $billId);
        if (!$bill) {
            return $data;
        }

        $data['sales_invoice_id'] = null;
        $data['expense_id'] = null;
        $data['ifrs16_lease_id'] = null;
        $data['ifrs16_schedule_id'] = null;
        $data['direction'] = 'out';
        $data['reference'] = app_normalize_whitespace((string) ($bill['bill_number'] ?? ''));
        $data['contact_code'] = app_normalize_whitespace((string) ($bill['vendor_code'] ?? ''));

        $amount = max(0, app_number($data['amount'] ?? 0));
        $remaining = app_purchase_bill_remaining_amount($bill);
        if ($amount <= 0 && $remaining > 0) {
            $data['amount'] = $remaining;
        }

        $data['offset_account_code'] = app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100']);

        return $data;
    }

    $expense = app_expense_by_id($companyId, $expenseId);
    if (!$expense) {
        return $data;
    }
    $data['sales_invoice_id'] = null;
    $data['purchase_bill_id'] = null;
    $data['ifrs16_lease_id'] = null;
    $data['ifrs16_schedule_id'] = null;
    $data['direction'] = 'out';
    $data['reference'] = app_normalize_whitespace((string) ($expense['expense_number'] ?? ''));
    $data['contact_code'] = '';

    $amount = max(0, app_number($data['amount'] ?? 0));
    $remaining = app_expense_remaining_amount($expense);
    if ($amount <= 0 && $remaining > 0) {
        $data['amount'] = $remaining;
    }

    $expenseOffset = app_normalize_whitespace((string) ($expense['offset_account_code'] ?? ''));
    if ($expenseOffset === '') {
        $expenseOffset = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
    }
    $data['offset_account_code'] = app_resolve_account_code($companyId, $expenseOffset, 'liability', ['2130', '2111', '2100']);

    return $data;
}

function app_normalize_prepaid_expense_payload(array $data, ?int $companyId): array
{
    $totalAmount = round(max(0, app_number($data['total_amount'] ?? 0)), 2);

    $isTaxable = !empty($data['is_taxable']) ? 1 : 0;
    $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
    $taxRate = max(0, app_number($data['tax_rate_percent'] ?? 0));
    if ($isTaxable === 1 && $companyId !== null && $companyId > 0) {
        if ($taxName !== '') {
            $row = app_tax_rate_row_by_name($companyId, $taxName, 'purchase');
            if ($row) {
                $taxRate = max(0, app_number($row['rate_percent'] ?? $taxRate));
            }
        } else {
            $defaultRate = app_default_tax_rate_row($companyId, 'purchase');
            if ($defaultRate) {
                $taxName = app_normalize_whitespace((string) ($defaultRate['tax_name'] ?? ''));
                $taxRate = max(0, app_number($defaultRate['rate_percent'] ?? $taxRate));
            }
        }
    }
    if ($isTaxable === 1 && $taxRate <= 0) {
        $taxRate = 15;
    }
    if ($isTaxable === 0) {
        $taxRate = 0;
        $taxName = '';
    }
    $taxTotal = $isTaxable === 1 ? round($totalAmount * ($taxRate / 100), 2) : 0.0;
    $grandTotal = round($totalAmount + $taxTotal, 2);

    $paidAmount = round(max(0, app_number($data['inline_paid_amount'] ?? 0)), 2);
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }

    $durationMonths = max(1, app_integer($data['duration_months'] ?? 1));
    $durationMonths = min(360, $durationMonths);
    $monthlyAmount = $durationMonths > 0 ? round($totalAmount / $durationMonths, 2) : 0.0;

    $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
    $paymentAccountName = app_normalize_whitespace((string) ($data['payment_account_name'] ?? ''));
    if ($paidAmount <= 0) {
        $paymentMethod = '';
        $paymentAccountName = '';
    }

    $prepaidAccountCode = app_normalize_whitespace((string) ($data['prepaid_account_code'] ?? ''));
    if ($prepaidAccountCode === '' && $companyId !== null && $companyId > 0) {
        $prepaidAccountCode = app_posting_account_code($companyId, 'prepaid_expense', 'asset', ['1150', '1100']);
    }

    $data['prepaid_account_code'] = $prepaidAccountCode;
    $data['total_amount'] = $totalAmount;
    $data['is_taxable'] = $isTaxable;
    $data['tax_rate_percent'] = $taxRate;
    $data['tax_name'] = $taxName;
    $data['tax_total'] = $taxTotal;
    $data['grand_total'] = $grandTotal;
    $data['inline_paid_amount'] = $paidAmount;
    $data['payment_method'] = $paymentMethod;
    $data['payment_account_name'] = $paymentAccountName;
    $data['duration_months'] = $durationMonths;
    $data['monthly_amount'] = $monthlyAmount;
    $data['start_date'] = app_sanitize_date((string) ($data['start_date'] ?? ''), date('Y-m-01'));
    $data['prepaid_date'] = app_sanitize_date((string) ($data['prepaid_date'] ?? ''), date('Y-m-d'));
    $data['amortized_amount'] = round(max(0, app_number($data['amortized_amount'] ?? 0)), 2);

    return $data;
}

function app_prepaid_expense_by_id(int $companyId, int $prepaidExpenseId): ?array
{
    if ($companyId <= 0 || $prepaidExpenseId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM prepaid_expenses
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'id' => $prepaidExpenseId,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_prepaid_expense_amortization_candidates(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        "SELECT *
         FROM prepaid_expenses
         WHERE company_id = :company_id
           AND status IN ('active', 'closed')
         ORDER BY start_date DESC, id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_ifrs16_normalize_rate_percent(mixed $value): float
{
    $rate = app_number($value);
    if ($rate > 0 && $rate <= 1) {
        $rate *= 100;
    }
    return round(max(0, $rate), 4);
}

function app_ifrs16_rate_decimal(mixed $percent): float
{
    $rate = app_number($percent);
    if ($rate > 1) {
        $rate /= 100;
    }
    return max(0.0, $rate);
}

function app_ifrs16_frequency_months(string $frequency): int
{
    return match (strtolower(app_normalize_whitespace($frequency))) {
        'quarterly' => 3,
        'semiannual', 'semi_annually', 'semi-annual' => 6,
        'annual', 'yearly' => 12,
        default => 1,
    };
}

function app_ifrs16_days_between(DateTimeImmutable $from, DateTimeImmutable $to): int
{
    return (int) $from->diff($to)->days;
}

function app_ifrs16_date(string $value, string $fallback): DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('Y-m-d', app_sanitize_date($value, $fallback));
    return $date ?: new DateTimeImmutable($fallback);
}

function app_ifrs16_signed_days_between(DateTimeImmutable $from, DateTimeImmutable $to): int
{
    return (int) $from->diff($to)->format('%r%a');
}

function app_ifrs16_default_first_payment_date(string $commencementDate): string
{
    $commencement = app_ifrs16_date($commencementDate, date('Y-m-d'));
    $firstPayment = $commencement->modify('last day of this month');
    if ($firstPayment <= $commencement) {
        $firstPayment = $commencement->modify('last day of next month');
    }
    return $firstPayment->format('Y-m-d');
}

function app_ifrs16_is_month_end(DateTimeImmutable $date): bool
{
    return $date->format('Y-m-d') === $date->modify('last day of this month')->format('Y-m-d');
}

function app_ifrs16_next_payment_date(DateTimeImmutable $date, int $months, bool $preserveMonthEnd, int $targetDay): DateTimeImmutable
{
    $months = max(1, $months);
    $targetMonth = $date->modify('first day of this month')->modify('+' . $months . ' months');
    if ($preserveMonthEnd) {
        return $targetMonth->modify('last day of this month');
    }

    $lastDay = (int) $targetMonth->modify('last day of this month')->format('d');
    $day = min(max(1, $targetDay), $lastDay);
    return $targetMonth->setDate((int) $targetMonth->format('Y'), (int) $targetMonth->format('m'), $day);
}

function app_ifrs16_parse_payment_date(mixed $value, string $fallback): string
{
    $text = app_normalize_whitespace((string) $value);
    if ($text === '') {
        return app_sanitize_date('', $fallback);
    }

    if (is_numeric($text)) {
        $serial = (float) $text;
        if ($serial >= 20000 && $serial <= 90000) {
            $timestamp = (int) round(($serial - 25569) * 86400);
            return gmdate('Y-m-d', $timestamp);
        }
    }

    if (preg_match('/^(\d{4})[\/\-.](\d{1,2})[\/\-.](\d{1,2})$/', $text, $matches)) {
        return sprintf('%04d-%02d-%02d', (int) $matches[1], (int) $matches[2], (int) $matches[3]);
    }

    if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/', $text, $matches)) {
        return sprintf('%04d-%02d-%02d', (int) $matches[3], (int) $matches[2], (int) $matches[1]);
    }

    return app_sanitize_date($text, $fallback);
}

function app_ifrs16_payment_dates(array $lease): array
{
    $commencement = app_ifrs16_date((string) ($lease['commencement_date'] ?? ''), date('Y-m-d'));
    $endDate = app_ifrs16_date((string) ($lease['end_date'] ?? ''), $commencement->format('Y-m-d'));
    if ($endDate < $commencement) {
        throw new RuntimeException('Lease end date must be on or after commencement date.');
    }

    $firstDefault = app_ifrs16_default_first_payment_date($commencement->format('Y-m-d'));
    $firstPayment = app_ifrs16_date((string) ($lease['first_payment_date'] ?? ''), $firstDefault);
    if ($firstPayment <= $commencement) {
        throw new RuntimeException('First payment date must be after the commencement date. Payments at or before commencement should be entered in the prepayment field.');
    }
    if ($firstPayment > $endDate) {
        throw new RuntimeException('First payment date must be on or before the lease end date.');
    }

    $months = app_ifrs16_frequency_months((string) ($lease['payment_frequency'] ?? 'monthly'));
    $requestedPayments = max(0, app_integer($lease['number_of_payments'] ?? 0));
    $limit = $requestedPayments > 0 ? min(480, $requestedPayments) : 480;
    $dates = [];
    $date = $firstPayment;
    $preserveMonthEnd = app_ifrs16_is_month_end($firstPayment);
    $targetDay = (int) $firstPayment->format('d');
    while ($date <= $endDate && count($dates) < $limit) {
        $dates[] = $date;
        $next = app_ifrs16_next_payment_date($date, $months, $preserveMonthEnd, $targetDay);
        if ($next <= $date) {
            break;
        }
        $date = $next;
    }

    if (!$dates) {
        throw new RuntimeException('No lease payment dates were generated.');
    }

    return $dates;
}

function app_ifrs16_prepare_payment_rows(array $lease, array $paymentRows): array
{
    $commencement = app_ifrs16_date((string) ($lease['commencement_date'] ?? ''), date('Y-m-d'));
    $endDate = app_ifrs16_date((string) ($lease['end_date'] ?? ''), $commencement->format('Y-m-d'));
    if ($endDate < $commencement) {
        throw new RuntimeException('Lease end date must be on or after commencement date.');
    }

    $benchmarkRate = app_ifrs16_normalize_rate_percent($lease['benchmark_rate_percent'] ?? 4.25);
    $creditSpread = app_ifrs16_normalize_rate_percent($lease['credit_spread_percent'] ?? 2);
    $manualRate = app_ifrs16_normalize_rate_percent($lease['manual_discount_rate_percent'] ?? 0);
    $discountRatePercent = $manualRate > 0 ? $manualRate : round($benchmarkRate + $creditSpread, 4);
    $discountRate = app_ifrs16_rate_decimal($discountRatePercent);

    $rows = [];
    foreach ($paymentRows as $index => $row) {
        if (!is_array($row)) {
            continue;
        }

        $fallbackDate = $commencement->format('Y-m-d');
        $paymentDate = app_ifrs16_parse_payment_date(
            $row['payment_date'] ?? $row['date'] ?? $row['schedule_date'] ?? '',
            $fallbackDate
        );
        $paymentDateObject = app_ifrs16_date($paymentDate, $fallbackDate);
        $paymentAmount = round(max(0, app_number($row['payment_amount'] ?? $row['amount'] ?? 0)), 2);
        if ($paymentAmount <= 0) {
            continue;
        }

        $days = app_ifrs16_signed_days_between($commencement, $paymentDateObject);
        $included = $paymentDateObject > $commencement && $paymentDateObject <= $endDate;
        $discountFactor = $included ? (1 / pow(1 + $discountRate, $days / 365)) : 0.0;
        $pvRaw = $included ? ($paymentAmount * $discountFactor) : 0.0;
        $rows[] = [
            'sequence_no' => app_integer($row['sequence_no'] ?? 0) > 0 ? app_integer($row['sequence_no']) : ($index + 1),
            'payment_date' => $paymentDateObject->format('Y-m-d'),
            'payment_amount' => $paymentAmount,
            'payment_type' => app_normalize_whitespace((string) ($row['payment_type'] ?? 'Fixed')),
            'included_in_liability' => $included ? 1 : 0,
            'days_from_commencement' => $included ? $days : 0,
            'discount_factor' => $included ? $discountFactor : 0.0,
            'pv_at_commencement' => round($pvRaw, 2),
            'pv_unrounded' => $pvRaw,
            'notes' => app_normalize_whitespace((string) ($row['notes'] ?? '')),
        ];
    }

    usort(
        $rows,
        static fn (array $left, array $right): int => [$left['payment_date'], $left['sequence_no']] <=> [$right['payment_date'], $right['sequence_no']]
    );

    $sequence = 1;
    foreach ($rows as &$row) {
        $row['sequence_no'] = $sequence++;
    }
    unset($row);

    return $rows;
}

function app_ifrs16_payment_rows_from_lease(array $lease): array
{
    $paymentAmount = round(max(0, app_number($lease['payment_amount'] ?? 0)), 2);
    if ($paymentAmount <= 0) {
        return [];
    }

    $rows = [];
    foreach (app_ifrs16_payment_dates($lease) as $index => $date) {
        if (!$date instanceof DateTimeImmutable) {
            continue;
        }
        $rows[] = [
            'sequence_no' => $index + 1,
            'payment_date' => $date->format('Y-m-d'),
            'payment_amount' => $paymentAmount,
            'payment_type' => 'Fixed',
            'notes' => 'Generated from lease frequency',
        ];
    }

    return app_ifrs16_prepare_payment_rows($lease, $rows);
}

function app_ifrs16_lease_payment_lines(int $companyId, int $leaseId): array
{
    if ($companyId <= 0 || $leaseId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM ifrs16_lease_payments
         WHERE company_id = :company_id
           AND lease_id = :lease_id
         ORDER BY sequence_no ASC, payment_date ASC, id ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'lease_id' => $leaseId]);
    return $stmt->fetchAll() ?: [];
}

function app_ifrs16_lease_payment_rows_for_calculation(int $companyId, int $leaseId, array $lease): array
{
    $savedRows = app_ifrs16_lease_payment_lines($companyId, $leaseId);
    if ($savedRows) {
        return app_ifrs16_prepare_payment_rows($lease, $savedRows);
    }
    return app_ifrs16_payment_rows_from_lease($lease);
}

function app_ifrs16_row_value(array $row, array $aliases): mixed
{
    foreach ($aliases as $alias) {
        $key = app_normalize_header_key((string) $alias);
        if ($key !== '' && array_key_exists($key, $row)) {
            return $row[$key];
        }
    }
    return null;
}

function app_ifrs16_import_payment_rows_from_file(array $lease, string $filePath): array
{
    if (!class_exists('TabularReader')) {
        throw new RuntimeException('Spreadsheet import reader is not available.');
    }

    $requiredHeaders = ['Payment Date', 'Payment Amount'];
    try {
        $structured = TabularReader::readWorksheet($filePath, 'Payment_Input', $requiredHeaders);
    } catch (Throwable) {
        $structured = TabularReader::read($filePath);
    }

    $rawRows = [];
    foreach ((array) ($structured['rows'] ?? []) as $row) {
        if (!is_array($row)) {
            continue;
        }

        $dateValue = app_ifrs16_row_value($row, ['payment_date', 'payment date', 'date', 'column_2']);
        $amountValue = app_ifrs16_row_value($row, ['payment_amount', 'payment amount', 'amount', 'column_3']);
        $amount = round(max(0, app_number($amountValue)), 2);
        if ($dateValue === null || trim((string) $dateValue) === '' || $amount <= 0) {
            continue;
        }

        $rawRows[] = [
            'payment_date' => $dateValue,
            'payment_amount' => $amount,
            'payment_type' => app_ifrs16_row_value($row, ['payment_type', 'payment type', 'type', 'column_4']) ?? 'Fixed',
            'notes' => app_ifrs16_row_value($row, ['notes', 'note', 'column_9']) ?? '',
        ];
    }

    if (!$rawRows) {
        throw new RuntimeException('No payment rows were found. Use columns Payment Date and Payment Amount, or the Payment_Input sheet from the IFRS16 workbook.');
    }

    return app_ifrs16_prepare_payment_rows($lease, $rawRows);
}

function app_ifrs16_assert_payment_rows_can_be_replaced(int $companyId, int $leaseId): void
{
    $initialJournal = app_journal_entry_for_source($companyId, 'ifrs16_leases', $leaseId);
    if ($initialJournal && strtolower(app_normalize_whitespace((string) ($initialJournal['status'] ?? ''))) === 'posted') {
        throw new RuntimeException('The initial IFRS16 journal entry is posted. Reverse or reset it before changing the payment input.');
    }

    foreach (app_ifrs16_lease_schedule_lines($companyId, $leaseId) as $scheduleLine) {
        $scheduleStatus = strtolower(app_normalize_whitespace((string) ($scheduleLine['status'] ?? 'draft')));
        $journalStatus = strtolower(app_normalize_whitespace((string) ($scheduleLine['journal_status'] ?? '')));
        if ($scheduleStatus === 'posted' || $journalStatus === 'posted') {
            throw new RuntimeException('One or more IFRS16 schedule entries are posted. Reverse or reset them before changing the payment input.');
        }
    }
}

function app_import_ifrs16_payment_schedule(int $companyId, int $leaseId, string $filePath): array
{
    if ($companyId <= 0 || $leaseId <= 0) {
        throw new RuntimeException('Select a valid IFRS16 lease.');
    }

    $lease = app_ifrs16_lease_by_id($companyId, $leaseId);
    if (!$lease) {
        throw new RuntimeException('IFRS16 lease was not found.');
    }

    app_ifrs16_assert_payment_rows_can_be_replaced($companyId, $leaseId);
    $paymentRows = app_ifrs16_import_payment_rows_from_file($lease, $filePath);
    $calculation = app_ifrs16_calculate_schedule($lease, $paymentRows);
    $summary = (array) ($calculation['summary'] ?? []);

    $pdo = app_pdo();
    $startedTransaction = false;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        foreach (app_ifrs16_lease_schedule_lines($companyId, $leaseId) as $scheduleLine) {
            app_delete_auto_journal_entry($companyId, 'ifrs16_lease_schedules', (int) ($scheduleLine['id'] ?? 0));
        }
        app_delete_auto_journal_entry($companyId, 'ifrs16_leases', $leaseId);

        $pdo->prepare(
            'DELETE FROM ifrs16_lease_schedules
             WHERE company_id = :company_id
               AND lease_id = :lease_id'
        )->execute(['company_id' => $companyId, 'lease_id' => $leaseId]);
        $pdo->prepare(
            'DELETE FROM ifrs16_lease_payments
             WHERE company_id = :company_id
               AND lease_id = :lease_id'
        )->execute(['company_id' => $companyId, 'lease_id' => $leaseId]);

        $insertStmt = $pdo->prepare(
            'INSERT INTO ifrs16_lease_payments
             (company_id, lease_id, sequence_no, payment_date, payment_amount, payment_type, included_in_liability, days_from_commencement, discount_factor, pv_at_commencement, notes)
             VALUES
             (:company_id, :lease_id, :sequence_no, :payment_date, :payment_amount, :payment_type, :included_in_liability, :days_from_commencement, :discount_factor, :pv_at_commencement, :notes)'
        );
        foreach ($paymentRows as $row) {
            $insertStmt->execute([
                'company_id' => $companyId,
                'lease_id' => $leaseId,
                'sequence_no' => (int) ($row['sequence_no'] ?? 0),
                'payment_date' => (string) ($row['payment_date'] ?? date('Y-m-d')),
                'payment_amount' => round(app_number($row['payment_amount'] ?? 0), 2),
                'payment_type' => app_normalize_whitespace((string) ($row['payment_type'] ?? 'Fixed')),
                'included_in_liability' => !empty($row['included_in_liability']) ? 1 : 0,
                'days_from_commencement' => app_integer($row['days_from_commencement'] ?? 0),
                'discount_factor' => round(app_number($row['discount_factor'] ?? 0), 10),
                'pv_at_commencement' => round(app_number($row['pv_at_commencement'] ?? 0), 2),
                'notes' => app_normalize_whitespace((string) ($row['notes'] ?? '')),
            ]);
        }

        $includedCount = 0;
        foreach ($paymentRows as $row) {
            if (!empty($row['included_in_liability'])) {
                $includedCount++;
            }
        }
        $leaseStatus = strtolower(app_normalize_whitespace((string) ($lease['status'] ?? 'draft')));
        if ($leaseStatus === 'active') {
            $leaseStatus = 'draft';
        }

        $pdo->prepare(
            'UPDATE ifrs16_leases
             SET number_of_payments = :number_of_payments,
                 payments_at_or_before_commencement = :payments_at_or_before_commencement,
                 benchmark_rate_percent = :benchmark_rate_percent,
                 credit_spread_percent = :credit_spread_percent,
                 manual_discount_rate_percent = :manual_discount_rate_percent,
                 discount_rate_percent = :discount_rate_percent,
                 lease_term_days = :lease_term_days,
                 lease_term_months = :lease_term_months,
                 pv_lease_liability = :pv_lease_liability,
                 initial_rou_asset = :initial_rou_asset,
                 daily_depreciation = :daily_depreciation,
                 monthly_depreciation = :monthly_depreciation,
                 total_interest = :total_interest,
                 closing_liability_check = :closing_liability_check,
                 journal_entry_id = NULL,
                 status = :status
             WHERE company_id = :company_id
               AND id = :id'
        )->execute([
            'number_of_payments' => $includedCount,
            'payments_at_or_before_commencement' => round(app_number($summary['payments_at_or_before_commencement'] ?? 0), 2),
            'benchmark_rate_percent' => (float) ($summary['benchmark_rate_percent'] ?? 0),
            'credit_spread_percent' => (float) ($summary['credit_spread_percent'] ?? 0),
            'manual_discount_rate_percent' => (float) ($summary['manual_discount_rate_percent'] ?? 0),
            'discount_rate_percent' => (float) ($summary['discount_rate_percent'] ?? 0),
            'lease_term_days' => (int) ($summary['lease_term_days'] ?? 0),
            'lease_term_months' => (int) ($summary['lease_term_months'] ?? 0),
            'pv_lease_liability' => round(app_number($summary['pv_lease_liability'] ?? 0), 2),
            'initial_rou_asset' => round(app_number($summary['initial_rou_asset'] ?? 0), 2),
            'daily_depreciation' => round(app_number($summary['daily_depreciation'] ?? 0), 2),
            'monthly_depreciation' => round(app_number($summary['monthly_depreciation'] ?? 0), 2),
            'total_interest' => round(app_number($summary['total_interest'] ?? 0), 2),
            'closing_liability_check' => round(app_number($summary['closing_liability_check'] ?? 0), 2),
            'status' => $leaseStatus,
            'company_id' => $companyId,
            'id' => $leaseId,
        ]);

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }

        return [
            'lease_id' => $leaseId,
            'rows_count' => count($paymentRows),
            'included_count' => $includedCount,
            'pv_lease_liability' => round(app_number($summary['pv_lease_liability'] ?? 0), 2),
            'initial_rou_asset' => round(app_number($summary['initial_rou_asset'] ?? 0), 2),
        ];
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_ifrs16_calculate_schedule(array $lease, ?array $paymentRows = null): array
{
    $commencement = app_ifrs16_date((string) ($lease['commencement_date'] ?? ''), date('Y-m-d'));
    $endDate = app_ifrs16_date((string) ($lease['end_date'] ?? ''), $commencement->format('Y-m-d'));
    if ($endDate < $commencement) {
        throw new RuntimeException('Lease end date must be on or after commencement date.');
    }

    $benchmarkRate = app_ifrs16_normalize_rate_percent($lease['benchmark_rate_percent'] ?? 4.25);
    $creditSpread = app_ifrs16_normalize_rate_percent($lease['credit_spread_percent'] ?? 2);
    $manualRate = app_ifrs16_normalize_rate_percent($lease['manual_discount_rate_percent'] ?? 0);
    $discountRatePercent = $manualRate > 0 ? $manualRate : round($benchmarkRate + $creditSpread, 4);
    $discountRate = app_ifrs16_rate_decimal($discountRatePercent);

    $termDays = app_ifrs16_days_between($commencement, $endDate) + 1;
    $termMonths = $termDays > 0 ? (int) ceil($termDays / 30.4375) : 0;
    $hasExplicitPaymentRows = $paymentRows !== null;
    $preparedPaymentRows = $hasExplicitPaymentRows
        ? app_ifrs16_prepare_payment_rows($lease, $paymentRows ?? [])
        : app_ifrs16_payment_rows_from_lease($lease);

    $pv = 0.0;
    $prepaymentsFromRows = 0.0;
    foreach ($preparedPaymentRows as $row) {
        if (!empty($row['included_in_liability'])) {
            $pv += app_number($row['pv_unrounded'] ?? ($row['pv_at_commencement'] ?? 0));
        } else {
            $paymentDate = app_ifrs16_date((string) ($row['payment_date'] ?? ''), $commencement->format('Y-m-d'));
            if ($paymentDate <= $commencement) {
                $prepaymentsFromRows += max(0, app_number($row['payment_amount'] ?? 0));
            }
        }
    }
    $pv = round($pv, 2);

    $manualPrepayments = round(max(0, app_number($lease['payments_at_or_before_commencement'] ?? 0)), 2);
    $prepayments = $hasExplicitPaymentRows && $prepaymentsFromRows > 0
        ? round($prepaymentsFromRows, 2)
        : $manualPrepayments;
    $initialDirectCosts = round(max(0, app_number($lease['initial_direct_costs'] ?? 0)), 2);
    $restorationCost = round(max(0, app_number($lease['restoration_cost'] ?? 0)), 2);
    $leaseIncentives = round(max(0, app_number($lease['lease_incentives'] ?? 0)), 2);
    $rawInitialRouAsset = $pv + $prepayments + $initialDirectCosts + $restorationCost - $leaseIncentives;
    if ($rawInitialRouAsset < -0.005) {
        throw new RuntimeException('Lease incentives cannot exceed the lease liability plus commencement costs.');
    }
    $initialRouAsset = round(max(0, $rawInitialRouAsset), 2);
    $dailyDepreciation = $termDays > 0 ? ($initialRouAsset / $termDays) : 0.0;
    $monthlyDepreciation = $termMonths > 0 ? round($initialRouAsset / $termMonths, 2) : 0.0;

    $rows = [];
    $opening = $pv;
    $previousDate = $commencement;
    $rouNbv = $initialRouAsset;
    $accumulatedDepreciation = 0.0;
    $totalInterest = 0.0;

    $sequence = 1;
    foreach ($preparedPaymentRows as $paymentRow) {
        if (empty($paymentRow['included_in_liability'])) {
            continue;
        }
        $date = app_ifrs16_date((string) ($paymentRow['payment_date'] ?? ''), $commencement->format('Y-m-d'));
        $payment = round(max(0, app_number($paymentRow['payment_amount'] ?? 0)), 2);

        if (abs($opening) < 0.005) {
            $opening = 0.0;
        }
        $days = app_ifrs16_days_between($previousDate, $date);
        $interest = $days > 0 ? ($opening * (pow(1 + $discountRate, $days / 365) - 1)) : 0.0;
        $closing = $opening + $interest - $payment;
        $liabilityReduction = $payment - $interest;
        $depreciation = $days > 0 ? min($rouNbv, $dailyDepreciation * $days) : 0.0;
        $rouNbv = max(0, $rouNbv - $depreciation);
        $accumulatedDepreciation += $depreciation;
        $totalInterest += $interest;

        $rows[] = [
            'sequence_no' => $sequence++,
            'schedule_date' => $date->format('Y-m-d'),
            'opening_liability' => round($opening, 2),
            'days_count' => $days,
            'interest_expense' => round(max(0, $interest), 2),
            'payment_amount' => round(max(0, $payment), 2),
            'liability_reduction' => round($liabilityReduction, 2),
            'closing_liability' => round($closing, 2),
            'depreciation_expense' => round(max(0, $depreciation), 2),
            'accumulated_depreciation' => round(max(0, $accumulatedDepreciation), 2),
            'rou_asset_nbv' => round(max(0, $rouNbv), 2),
        ];

        $opening = $closing;
        $previousDate = $date;
    }

    return [
        'summary' => [
            'benchmark_rate_percent' => $benchmarkRate,
            'credit_spread_percent' => $creditSpread,
            'manual_discount_rate_percent' => $manualRate,
            'discount_rate_percent' => $discountRatePercent,
            'lease_term_days' => $termDays,
            'lease_term_months' => $termMonths,
            'payments_at_or_before_commencement' => $prepayments,
            'pv_lease_liability' => $pv,
            'initial_rou_asset' => $initialRouAsset,
            'daily_depreciation' => round($dailyDepreciation, 2),
            'monthly_depreciation' => $monthlyDepreciation,
            'total_interest' => round($totalInterest, 2),
            'closing_liability_check' => round($opening, 2),
        ],
        'rows' => $rows,
    ];
}

function app_normalize_ifrs16_lease_payload(array $data, ?int $companyId): array
{
    $recordId = app_integer($data['__record_id'] ?? ($data['id'] ?? 0));
    unset($data['__record_id']);

    $commencementDate = app_sanitize_date((string) ($data['commencement_date'] ?? ''), date('Y-m-d'));
    $endDate = app_sanitize_date((string) ($data['end_date'] ?? ''), $commencementDate);
    if ($endDate < $commencementDate) {
        [$commencementDate, $endDate] = [$endDate, $commencementDate];
    }
    $firstPaymentDefault = app_ifrs16_default_first_payment_date($commencementDate);
    $firstPaymentDate = app_sanitize_date((string) ($data['first_payment_date'] ?? ''), $firstPaymentDefault);

    $data['country'] = app_normalize_whitespace((string) ($data['country'] ?? 'Saudi Arabia')) ?: 'Saudi Arabia';
    $data['currency_code'] = strtoupper(app_normalize_whitespace((string) ($data['currency_code'] ?? 'SAR'))) ?: 'SAR';
    $data['commencement_date'] = $commencementDate;
    $data['end_date'] = $endDate;
    $data['first_payment_date'] = $firstPaymentDate;
    $data['payment_frequency'] = strtolower(app_normalize_whitespace((string) ($data['payment_frequency'] ?? 'monthly'))) ?: 'monthly';
    if (!in_array($data['payment_frequency'], ['monthly', 'quarterly', 'semiannual', 'annual'], true)) {
        $data['payment_frequency'] = 'monthly';
    }
    $data['payment_amount'] = round(max(0, app_number($data['payment_amount'] ?? 0)), 2);
    $data['number_of_payments'] = min(480, max(0, app_integer($data['number_of_payments'] ?? 0)));
    foreach (['payments_at_or_before_commencement', 'initial_direct_costs', 'restoration_cost', 'lease_incentives'] as $amountField) {
        $data[$amountField] = round(max(0, app_number($data[$amountField] ?? 0)), 2);
    }

    if ($companyId !== null && $companyId > 0) {
        $defaults = [
            'rou_asset_account_code' => ['ifrs16_rou_asset', 'asset', ['1231', '1220', '1210']],
            'lease_liability_account_code' => ['ifrs16_lease_liability', 'liability', ['2210', '2200', '2130']],
            'interest_expense_account_code' => ['ifrs16_interest_expense', 'expense', ['5700', '5310', '5600']],
            'depreciation_expense_account_code' => ['ifrs16_depreciation_expense', 'expense', ['5600', '5700', '5310']],
            'accumulated_depreciation_account_code' => ['ifrs16_accumulated_depreciation', 'asset', ['1239', '1220', '1210']],
            'restoration_provision_account_code' => ['ifrs16_restoration_provision', 'liability', ['2220', '2210', '2130']],
            'lease_incentive_account_code' => ['ifrs16_lease_incentive', 'asset', ['1160', '1120', '1112']],
        ];
        foreach ($defaults as $field => [$settingKey, $expectedType, $fallbacks]) {
            $code = app_normalize_whitespace((string) ($data[$field] ?? ''));
            $data[$field] = $code !== ''
                ? app_resolve_account_code($companyId, $code, $expectedType, $fallbacks)
                : app_posting_account_code($companyId, $settingKey, $expectedType, $fallbacks);
        }
    }

    $paymentRows = ($companyId !== null && $companyId > 0 && $recordId > 0)
        ? app_ifrs16_lease_payment_rows_for_calculation($companyId, $recordId, $data)
        : null;
    $calculation = app_ifrs16_calculate_schedule($data, $paymentRows);
    foreach ((array) ($calculation['summary'] ?? []) as $key => $value) {
        $data[$key] = $value;
    }
    $data['status'] = app_normalize_whitespace((string) ($data['status'] ?? 'draft')) ?: 'draft';
    if (!in_array($data['status'], ['draft', 'active', 'closed', 'cancelled'], true)) {
        $data['status'] = 'draft';
    }

    return $data;
}

function app_ifrs16_lease_by_id(int $companyId, int $leaseId): ?array
{
    if ($companyId <= 0 || $leaseId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM ifrs16_leases
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $leaseId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_ifrs16_schedule_by_id(int $companyId, int $scheduleId): ?array
{
    if ($companyId <= 0 || $scheduleId <= 0) {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM ifrs16_lease_schedules
         WHERE company_id = :company_id
           AND id = :id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $scheduleId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_ifrs16_schedule_paid_amount(int $companyId, int $scheduleId, ?int $excludePaymentId = null): float
{
    if ($companyId <= 0 || $scheduleId <= 0) {
        return 0.0;
    }

    $sql = "SELECT COALESCE(SUM(amount), 0)
            FROM payments
            WHERE company_id = :company_id
              AND ifrs16_schedule_id = :schedule_id
              AND direction = 'out'";
    $params = [
        'company_id' => $companyId,
        'schedule_id' => $scheduleId,
    ];
    if ($excludePaymentId !== null && $excludePaymentId > 0) {
        $sql .= ' AND id <> :exclude_payment_id';
        $params['exclude_payment_id'] = $excludePaymentId;
    }

    $stmt = app_pdo()->prepare($sql);
    $stmt->execute($params);
    return round(max(0, app_number($stmt->fetchColumn() ?: 0)), 2);
}

function app_ifrs16_schedule_remaining_amount(int $companyId, array $schedule, ?int $excludePaymentId = null): float
{
    $scheduleId = (int) ($schedule['id'] ?? 0);
    if ($companyId <= 0 || $scheduleId <= 0) {
        return 0.0;
    }

    $paymentAmount = round(max(0, app_number($schedule['payment_amount'] ?? 0)), 2);
    $paidAmount = app_ifrs16_schedule_paid_amount($companyId, $scheduleId, $excludePaymentId);
    return round(max(0, $paymentAmount - $paidAmount), 2);
}

function app_build_ifrs16_initial_journal_lines(int $companyId, array $lease): array
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for IFRS16 posting.');
    }

    $leaseNumber = app_normalize_whitespace((string) ($lease['lease_number'] ?? ''));
    $rouAsset = round(max(0, app_number($lease['initial_rou_asset'] ?? 0)), 2);
    $liability = round(max(0, app_number($lease['pv_lease_liability'] ?? 0)), 2);
    $prepayments = round(max(0, app_number($lease['payments_at_or_before_commencement'] ?? 0)), 2);
    $initialDirectCosts = round(max(0, app_number($lease['initial_direct_costs'] ?? 0)), 2);
    $restorationCost = round(max(0, app_number($lease['restoration_cost'] ?? 0)), 2);
    $leaseIncentives = round(max(0, app_number($lease['lease_incentives'] ?? 0)), 2);
    $commencementClearingCredits = round($prepayments + $initialDirectCosts, 2);

    $rouAssetAccount = app_resolve_account_code($companyId, (string) ($lease['rou_asset_account_code'] ?? ''), 'asset', ['1231', '1220', '1210']);
    $liabilityAccount = app_resolve_account_code($companyId, (string) ($lease['lease_liability_account_code'] ?? ''), 'liability', ['2210', '2200', '2130']);
    $prepaidClearingAccount = app_posting_account_code($companyId, 'prepaid_expense', 'asset', ['1150', '1160', '1100']);
    $restorationAccount = app_resolve_account_code($companyId, (string) ($lease['restoration_provision_account_code'] ?? ''), 'liability', ['2220', '2210', '2130']);
    $incentiveAccount = app_resolve_account_code($companyId, (string) ($lease['lease_incentive_account_code'] ?? ''), 'asset', ['1160', '1120', '1112']);

    $lines = [];
    if ($rouAsset > 0) {
        $lines[] = app_accounting_line($rouAssetAccount, $rouAsset, 0, 'IFRS16 ROU asset recognition ' . $leaseNumber);
    }
    if ($leaseIncentives > 0) {
        $lines[] = app_accounting_line($incentiveAccount, $leaseIncentives, 0, 'IFRS16 lease incentive receivable ' . $leaseNumber);
    }
    if ($liability > 0) {
        $lines[] = app_accounting_line($liabilityAccount, 0, $liability, 'IFRS16 lease liability recognition ' . $leaseNumber);
    }
    if ($commencementClearingCredits > 0) {
        $lines[] = app_accounting_line($prepaidClearingAccount, 0, $commencementClearingCredits, 'IFRS16 reclass commencement payments and direct costs ' . $leaseNumber);
    }
    if ($restorationCost > 0) {
        $lines[] = app_accounting_line($restorationAccount, 0, $restorationCost, 'IFRS16 restoration provision ' . $leaseNumber);
    }

    return $lines;
}

function app_build_ifrs16_schedule_journal_lines(int $companyId, array $schedule): array
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for IFRS16 schedule posting.');
    }

    $leaseId = app_integer($schedule['lease_id'] ?? 0);
    $lease = app_ifrs16_lease_by_id($companyId, $leaseId);
    if (!$lease) {
        throw new RuntimeException('Linked IFRS16 lease was not found.');
    }

    $leaseNumber = app_normalize_whitespace((string) ($schedule['lease_number'] ?? ($lease['lease_number'] ?? '')));
    $sequenceNo = app_integer($schedule['sequence_no'] ?? 0);
    $suffix = $sequenceNo > 0 ? (' #' . str_pad((string) $sequenceNo, 3, '0', STR_PAD_LEFT)) : '';
    $costCenterCode = app_normalize_whitespace((string) ($lease['cost_center_code'] ?? ''));
    if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
        throw new RuntimeException('Cost center is required for IFRS16 schedule posting.');
    }

    $interestAccount = app_resolve_account_code($companyId, (string) ($lease['interest_expense_account_code'] ?? ''), 'expense', ['5700', '5310', '5600']);
    $liabilityAccount = app_resolve_account_code($companyId, (string) ($lease['lease_liability_account_code'] ?? ''), 'liability', ['2210', '2200', '2130']);
    $depreciationAccount = app_resolve_account_code($companyId, (string) ($lease['depreciation_expense_account_code'] ?? ''), 'expense', ['5600', '5700', '5310']);
    $accumulatedAccount = app_resolve_account_code($companyId, (string) ($lease['accumulated_depreciation_account_code'] ?? ''), 'asset', ['1239', '1220', '1210']);

    $interest = round(max(0, app_number($schedule['interest_expense'] ?? 0)), 2);
    $depreciation = round(max(0, app_number($schedule['depreciation_expense'] ?? 0)), 2);
    $label = trim($leaseNumber . $suffix);

    $lines = [];
    if ($interest > 0) {
        $lines[] = app_accounting_line($interestAccount, $interest, 0, 'IFRS16 interest expense ' . $label, $costCenterCode);
        $lines[] = app_accounting_line($liabilityAccount, 0, $interest, 'IFRS16 lease liability interest accrual ' . $label);
    }
    if ($depreciation > 0) {
        $lines[] = app_accounting_line($depreciationAccount, $depreciation, 0, 'IFRS16 ROU depreciation ' . $label, $costCenterCode);
        $lines[] = app_accounting_line($accumulatedAccount, 0, $depreciation, 'IFRS16 accumulated ROU depreciation ' . $label);
    }

    return $lines;
}

function app_ifrs16_lease_schedule_lines(int $companyId, int $leaseId): array
{
    if ($companyId <= 0 || $leaseId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        "SELECT s.*,
                COALESCE(j.entry_number, '') AS journal_entry_number,
                COALESCE(j.status, '') AS journal_status
         FROM ifrs16_lease_schedules s
         LEFT JOIN journal_entries j
           ON j.company_id = s.company_id
          AND j.source_type = 'ifrs16_lease_schedules'
          AND j.source_id = s.id
         WHERE s.company_id = :company_id
           AND s.lease_id = :lease_id
         ORDER BY s.sequence_no ASC, s.id ASC"
    );
    $stmt->execute(['company_id' => $companyId, 'lease_id' => $leaseId]);
    return $stmt->fetchAll() ?: [];
}

function app_generate_ifrs16_lease_schedule(int $companyId, int $leaseId): array
{
    if ($companyId <= 0 || $leaseId <= 0) {
        throw new RuntimeException('Select a valid IFRS16 lease.');
    }

    $pdo = app_pdo();
    $startedTransaction = false;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        $stmt = $pdo->prepare(
            'SELECT *
             FROM ifrs16_leases
             WHERE company_id = :company_id
               AND id = :id
             LIMIT 1'
        );
        $stmt->execute(['company_id' => $companyId, 'id' => $leaseId]);
        $lease = $stmt->fetch();
        if (!$lease) {
            throw new RuntimeException('IFRS16 lease was not found.');
        }

        $currentStatus = strtolower(app_normalize_whitespace((string) ($lease['status'] ?? 'draft')));
        if (in_array($currentStatus, ['closed', 'cancelled'], true)) {
            throw new RuntimeException('Closed or cancelled leases cannot be regenerated.');
        }

        $initialJournal = app_journal_entry_for_source($companyId, 'ifrs16_leases', $leaseId);
        if ($initialJournal && strtolower(app_normalize_whitespace((string) ($initialJournal['status'] ?? ''))) === 'posted') {
            throw new RuntimeException('The initial IFRS16 journal entry is posted. Reverse or reset it before regenerating.');
        }

        $existingSchedules = app_ifrs16_lease_schedule_lines($companyId, $leaseId);
        foreach ($existingSchedules as $existingSchedule) {
            $scheduleStatus = strtolower(app_normalize_whitespace((string) ($existingSchedule['status'] ?? 'draft')));
            $journalStatus = strtolower(app_normalize_whitespace((string) ($existingSchedule['journal_status'] ?? '')));
            if ($scheduleStatus === 'posted' || $journalStatus === 'posted') {
                throw new RuntimeException('One or more IFRS16 schedule entries are posted. Reset them before regenerating.');
            }
        }

        $lease = app_normalize_ifrs16_lease_payload($lease, $companyId);
        app_validate_transaction_master_links('ifrs16_leases', $lease, $companyId);
        $paymentRowsForCalculation = app_ifrs16_lease_payment_rows_for_calculation($companyId, $leaseId, $lease);
        $calculation = app_ifrs16_calculate_schedule($lease, $paymentRowsForCalculation);
        $summary = (array) ($calculation['summary'] ?? []);
        $scheduleRows = (array) ($calculation['rows'] ?? []);
        if (!$scheduleRows) {
            throw new RuntimeException('No IFRS16 schedule rows were generated.');
        }
        $includedPaymentRows = 0;
        foreach ($paymentRowsForCalculation as $paymentInputRow) {
            if (!empty($paymentInputRow['included_in_liability'])) {
                $includedPaymentRows++;
            }
        }

        foreach ($existingSchedules as $existingSchedule) {
            app_delete_auto_journal_entry($companyId, 'ifrs16_lease_schedules', (int) ($existingSchedule['id'] ?? 0));
        }
        $pdo->prepare(
            'DELETE FROM ifrs16_lease_schedules
             WHERE company_id = :company_id
               AND lease_id = :lease_id'
        )->execute(['company_id' => $companyId, 'lease_id' => $leaseId]);

        $leaseStatusAfterGenerate = $currentStatus === 'draft' ? 'active' : $currentStatus;
        $updateLeaseStmt = $pdo->prepare(
            'UPDATE ifrs16_leases
             SET country = :country,
                 currency_code = :currency_code,
                 commencement_date = :commencement_date,
                 end_date = :end_date,
                 first_payment_date = :first_payment_date,
                 payment_frequency = :payment_frequency,
                 payment_amount = :payment_amount,
                 number_of_payments = :number_of_payments,
                 payments_at_or_before_commencement = :payments_at_or_before_commencement,
                 initial_direct_costs = :initial_direct_costs,
                 restoration_cost = :restoration_cost,
                 lease_incentives = :lease_incentives,
                 benchmark_rate_percent = :benchmark_rate_percent,
                 credit_spread_percent = :credit_spread_percent,
                 manual_discount_rate_percent = :manual_discount_rate_percent,
                 discount_rate_percent = :discount_rate_percent,
                 lease_term_days = :lease_term_days,
                 lease_term_months = :lease_term_months,
                 pv_lease_liability = :pv_lease_liability,
                 initial_rou_asset = :initial_rou_asset,
                 daily_depreciation = :daily_depreciation,
                 monthly_depreciation = :monthly_depreciation,
                 total_interest = :total_interest,
                 closing_liability_check = :closing_liability_check,
                 rou_asset_account_code = :rou_asset_account_code,
                 lease_liability_account_code = :lease_liability_account_code,
                 interest_expense_account_code = :interest_expense_account_code,
                 depreciation_expense_account_code = :depreciation_expense_account_code,
                 accumulated_depreciation_account_code = :accumulated_depreciation_account_code,
                 restoration_provision_account_code = :restoration_provision_account_code,
                 lease_incentive_account_code = :lease_incentive_account_code,
                 cost_center_code = :cost_center_code,
                 status = :status
             WHERE id = :id
               AND company_id = :company_id'
        );
        $updateLeaseStmt->execute([
            'country' => (string) ($lease['country'] ?? 'Saudi Arabia'),
            'currency_code' => (string) ($lease['currency_code'] ?? 'SAR'),
            'commencement_date' => (string) ($lease['commencement_date'] ?? date('Y-m-d')),
            'end_date' => (string) ($lease['end_date'] ?? date('Y-m-d')),
            'first_payment_date' => (string) ($lease['first_payment_date'] ?? date('Y-m-d')),
            'payment_frequency' => (string) ($lease['payment_frequency'] ?? 'monthly'),
            'payment_amount' => round(max(0, app_number($lease['payment_amount'] ?? 0)), 2),
            'number_of_payments' => $includedPaymentRows > 0 ? $includedPaymentRows : max(0, app_integer($lease['number_of_payments'] ?? 0)),
            'payments_at_or_before_commencement' => round(max(0, app_number($summary['payments_at_or_before_commencement'] ?? $lease['payments_at_or_before_commencement'] ?? 0)), 2),
            'initial_direct_costs' => round(max(0, app_number($lease['initial_direct_costs'] ?? 0)), 2),
            'restoration_cost' => round(max(0, app_number($lease['restoration_cost'] ?? 0)), 2),
            'lease_incentives' => round(max(0, app_number($lease['lease_incentives'] ?? 0)), 2),
            'benchmark_rate_percent' => (float) ($summary['benchmark_rate_percent'] ?? $lease['benchmark_rate_percent'] ?? 0),
            'credit_spread_percent' => (float) ($summary['credit_spread_percent'] ?? $lease['credit_spread_percent'] ?? 0),
            'manual_discount_rate_percent' => (float) ($summary['manual_discount_rate_percent'] ?? $lease['manual_discount_rate_percent'] ?? 0),
            'discount_rate_percent' => (float) ($summary['discount_rate_percent'] ?? 0),
            'lease_term_days' => (int) ($summary['lease_term_days'] ?? 0),
            'lease_term_months' => (int) ($summary['lease_term_months'] ?? 0),
            'pv_lease_liability' => round(app_number($summary['pv_lease_liability'] ?? 0), 2),
            'initial_rou_asset' => round(app_number($summary['initial_rou_asset'] ?? 0), 2),
            'daily_depreciation' => round(app_number($summary['daily_depreciation'] ?? 0), 2),
            'monthly_depreciation' => round(app_number($summary['monthly_depreciation'] ?? 0), 2),
            'total_interest' => round(app_number($summary['total_interest'] ?? 0), 2),
            'closing_liability_check' => round(app_number($summary['closing_liability_check'] ?? 0), 2),
            'rou_asset_account_code' => (string) ($lease['rou_asset_account_code'] ?? ''),
            'lease_liability_account_code' => (string) ($lease['lease_liability_account_code'] ?? ''),
            'interest_expense_account_code' => (string) ($lease['interest_expense_account_code'] ?? ''),
            'depreciation_expense_account_code' => (string) ($lease['depreciation_expense_account_code'] ?? ''),
            'accumulated_depreciation_account_code' => (string) ($lease['accumulated_depreciation_account_code'] ?? ''),
            'restoration_provision_account_code' => (string) ($lease['restoration_provision_account_code'] ?? ''),
            'lease_incentive_account_code' => (string) ($lease['lease_incentive_account_code'] ?? ''),
            'cost_center_code' => (string) ($lease['cost_center_code'] ?? ''),
            'status' => $leaseStatusAfterGenerate,
            'id' => $leaseId,
            'company_id' => $companyId,
        ]);

        app_upsert_auto_journal_entry(
            $companyId,
            'ifrs16_leases',
            $leaseId,
            (string) ($lease['commencement_date'] ?? date('Y-m-d')),
            (string) ($lease['lease_number'] ?? ''),
            'IFRS16 initial recognition ' . (string) ($lease['lease_number'] ?? ''),
            app_build_ifrs16_initial_journal_lines($companyId, array_merge($lease, $summary)),
            'draft'
        );
        $initialJournal = app_journal_entry_for_source($companyId, 'ifrs16_leases', $leaseId);
        $initialJournalId = (int) ($initialJournal['id'] ?? 0);
        if ($initialJournalId > 0) {
            $pdo->prepare(
                'UPDATE ifrs16_leases
                 SET journal_entry_id = :journal_entry_id
                 WHERE id = :id
                   AND company_id = :company_id'
            )->execute([
                'journal_entry_id' => $initialJournalId,
                'id' => $leaseId,
                'company_id' => $companyId,
            ]);
        }

        $insertScheduleStmt = $pdo->prepare(
            'INSERT INTO ifrs16_lease_schedules
             (company_id, lease_id, lease_number, sequence_no, schedule_date, opening_liability, days_count, interest_expense, payment_amount, liability_reduction, closing_liability, depreciation_expense, accumulated_depreciation, rou_asset_nbv, status, notes)
             VALUES
             (:company_id, :lease_id, :lease_number, :sequence_no, :schedule_date, :opening_liability, :days_count, :interest_expense, :payment_amount, :liability_reduction, :closing_liability, :depreciation_expense, :accumulated_depreciation, :rou_asset_nbv, :status, :notes)'
        );
        $createdScheduleIds = [];
        foreach ($scheduleRows as $row) {
            $insertScheduleStmt->execute([
                'company_id' => $companyId,
                'lease_id' => $leaseId,
                'lease_number' => (string) ($lease['lease_number'] ?? ''),
                'sequence_no' => (int) ($row['sequence_no'] ?? 0),
                'schedule_date' => (string) ($row['schedule_date'] ?? date('Y-m-d')),
                'opening_liability' => round(app_number($row['opening_liability'] ?? 0), 2),
                'days_count' => max(0, app_integer($row['days_count'] ?? 0)),
                'interest_expense' => round(app_number($row['interest_expense'] ?? 0), 2),
                'payment_amount' => round(app_number($row['payment_amount'] ?? 0), 2),
                'liability_reduction' => round(app_number($row['liability_reduction'] ?? 0), 2),
                'closing_liability' => round(app_number($row['closing_liability'] ?? 0), 2),
                'depreciation_expense' => round(app_number($row['depreciation_expense'] ?? 0), 2),
                'accumulated_depreciation' => round(app_number($row['accumulated_depreciation'] ?? 0), 2),
                'rou_asset_nbv' => round(app_number($row['rou_asset_nbv'] ?? 0), 2),
                'status' => 'draft',
                'notes' => '',
            ]);
            $scheduleId = (int) $pdo->lastInsertId();
            $createdScheduleIds[] = $scheduleId;

            $scheduleRecord = app_module_find('ifrs16_lease_schedules', $scheduleId, $companyId);
            if ($scheduleRecord) {
                app_sync_auto_posting_for_record('ifrs16_lease_schedules', $scheduleRecord, $companyId);
            }
        }

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }

        return [
            'lease_id' => $leaseId,
            'lease_number' => (string) ($lease['lease_number'] ?? ''),
            'schedules_count' => count($createdScheduleIds),
            'pv_lease_liability' => round(app_number($summary['pv_lease_liability'] ?? 0), 2),
            'initial_rou_asset' => round(app_number($summary['initial_rou_asset'] ?? 0), 2),
            'total_interest' => round(app_number($summary['total_interest'] ?? 0), 2),
            'closing_liability_check' => round(app_number($summary['closing_liability_check'] ?? 0), 2),
            'journal_entry_id' => $initialJournalId,
            'journal_entry_number' => (string) ($initialJournal['entry_number'] ?? ''),
        ];
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_student_account_settings(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT *
         FROM student_account_settings
         WHERE company_id = :company_id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId]);
    $settings = $stmt->fetch();
    if ($settings) {
        return $settings;
    }

    $pdo->prepare(
        'INSERT IGNORE INTO student_account_settings
         (company_id, receivable_account_code, advance_account_code, discount_account_code, default_revenue_account_code, default_cost_center_code)
         VALUES (:company_id, :receivable, :advance, :discount, :revenue, :cost_center)'
    )->execute([
        'company_id' => $companyId,
        'receivable' => '1121',
        'advance' => '2140',
        'discount' => '2140',
        'revenue' => '4100',
        'cost_center' => 'CC-ADM',
    ]);

    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetch() ?: [];
}

function app_student_setting_account_code(int $companyId, string $settingKey, string $expectedType, array $preferredFallbacks): string
{
    $settings = app_student_account_settings($companyId);
    $field = match ($settingKey) {
        'student_receivable' => 'receivable_account_code',
        'student_advance' => 'advance_account_code',
        'student_discount' => 'discount_account_code',
        'student_revenue' => 'default_revenue_account_code',
        default => '',
    };
    $requestedCode = $field !== '' ? app_normalize_whitespace((string) ($settings[$field] ?? '')) : '';
    return app_resolve_account_code($companyId, $requestedCode, $expectedType, $preferredFallbacks);
}

function app_student_stage_exists(int $companyId, string $stageCode): bool
{
    $stageCode = app_normalize_whitespace($stageCode);
    if ($companyId <= 0 || $stageCode === '') {
        return false;
    }
    $stmt = app_pdo()->prepare(
        'SELECT 1 FROM student_stages WHERE company_id = :company_id AND stage_code = :stage_code AND is_active = 1 LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'stage_code' => $stageCode]);
    return (bool) $stmt->fetchColumn();
}

function app_student_grade_row(int $companyId, string $gradeCode): ?array
{
    $gradeCode = app_normalize_whitespace($gradeCode);
    if ($companyId <= 0 || $gradeCode === '') {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT * FROM student_grades WHERE company_id = :company_id AND grade_code = :grade_code AND is_active = 1 LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'grade_code' => $gradeCode]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_student_class_row(int $companyId, string $classCode): ?array
{
    $classCode = app_normalize_whitespace($classCode);
    if ($companyId <= 0 || $classCode === '') {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT * FROM student_classes WHERE company_id = :company_id AND class_code = :class_code AND is_active = 1 LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'class_code' => $classCode]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_student_by_number(int $companyId, string $studentNumber): ?array
{
    $studentNumber = app_normalize_whitespace($studentNumber);
    if ($companyId <= 0 || $studentNumber === '') {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT * FROM students WHERE company_id = :company_id AND student_number = :student_number LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'student_number' => $studentNumber]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_student_revenue_type_by_code(int $companyId, string $revenueTypeCode): ?array
{
    $revenueTypeCode = app_normalize_whitespace($revenueTypeCode);
    if ($companyId <= 0 || $revenueTypeCode === '') {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT * FROM student_revenue_types
         WHERE company_id = :company_id
           AND revenue_type_code = :revenue_type_code
           AND is_active = 1
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'revenue_type_code' => $revenueTypeCode]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_student_invoice_remaining_amount(array $invoice): float
{
    $grandTotal = max(0, app_number($invoice['grand_total'] ?? 0));
    $paidAmount = max(0, app_number($invoice['paid_amount'] ?? 0));
    return round(max(0, $grandTotal - $paidAmount), 2);
}

function app_student_invoice_by_id(int $companyId, int $invoiceId): ?array
{
    if ($companyId <= 0 || $invoiceId <= 0) {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT * FROM student_invoices WHERE company_id = :company_id AND id = :id LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $invoiceId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_student_invoice_payment_candidates(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        "SELECT *
         FROM student_invoices
         WHERE company_id = :company_id
           AND status NOT IN ('draft', 'cancelled')
         ORDER BY invoice_date DESC, id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_normalize_student_invoice_payload(array $data, ?int $companyId): array
{
    $companyId = (int) ($companyId ?? 0);
    if ($companyId > 0) {
        $student = app_student_by_number($companyId, (string) ($data['student_number'] ?? ''));
        if ($student) {
            foreach (['guardian_contact_code', 'stage_code', 'grade_code', 'class_code'] as $field) {
                if (app_normalize_whitespace((string) ($data[$field] ?? '')) === '') {
                    $data[$field] = (string) ($student[$field] ?? '');
                }
            }
        }

        $revenueType = app_student_revenue_type_by_code($companyId, (string) ($data['revenue_type_code'] ?? ''));
        if ($revenueType) {
            if (app_normalize_whitespace((string) ($data['description'] ?? '')) === '') {
                $data['description'] = (string) ($revenueType['revenue_type_name'] ?? '');
            }
            if (app_normalize_whitespace((string) ($data['revenue_account_code'] ?? '')) === '') {
                $data['revenue_account_code'] = (string) ($revenueType['revenue_account_code'] ?? '');
            }
            if (app_normalize_whitespace((string) ($data['cost_center_code'] ?? '')) === '') {
                $data['cost_center_code'] = (string) ($revenueType['cost_center_code'] ?? '');
            }
            if (empty($data['is_taxable'])) {
                $data['is_taxable'] = (int) ($revenueType['is_taxable'] ?? 0);
            }
            if (app_normalize_whitespace((string) ($data['tax_name'] ?? '')) === '') {
                $data['tax_name'] = (string) ($revenueType['tax_name'] ?? '');
            }
            if (max(0, app_number($data['subtotal'] ?? 0)) <= 0 && max(0, app_number($revenueType['default_amount'] ?? 0)) > 0) {
                $data['subtotal'] = (float) $revenueType['default_amount'];
            }
        }

        $settings = app_student_account_settings($companyId);
        if (app_normalize_whitespace((string) ($data['revenue_account_code'] ?? '')) === '') {
            $data['revenue_account_code'] = (string) ($settings['default_revenue_account_code'] ?? '');
        }
        if (app_normalize_whitespace((string) ($data['cost_center_code'] ?? '')) === '') {
            $data['cost_center_code'] = (string) ($settings['default_cost_center_code'] ?? '');
        }
    }

    return app_normalize_invoice_bill_payload('sales_invoices', $data, $companyId);
}

function app_document_line_slugs(): array
{
    return ['sales_invoices', 'purchase_bills', 'expenses', 'sales_returns', 'purchase_returns', 'expense_returns', 'student_invoices'];
}

function app_document_line_config(string $slug): ?array
{
    return match ($slug) {
        'sales_invoices' => [
            'table' => 'sales_invoice_lines',
            'parent_column' => 'sales_invoice_id',
            'account_type' => 'revenue',
            'account_field' => 'revenue_account_code',
            'tax_scope' => 'sales',
            'label' => 'Invoice line',
        ],
        'purchase_bills' => [
            'table' => 'purchase_bill_lines',
            'parent_column' => 'purchase_bill_id',
            'account_type' => 'expense',
            'account_field' => 'expense_account_code',
            'tax_scope' => 'purchase',
            'label' => 'Bill line',
        ],
        'expenses' => [
            'table' => 'expense_lines',
            'parent_column' => 'expense_id',
            'account_type' => 'expense',
            'account_field' => 'expense_account_code',
            'tax_scope' => 'purchase',
            'label' => 'Expense line',
        ],
        'sales_returns' => [
            'table' => 'sales_return_lines',
            'parent_column' => 'sales_return_id',
            'account_type' => 'revenue',
            'account_field' => 'revenue_account_code',
            'tax_scope' => 'sales',
            'label' => 'Sales return line',
        ],
        'purchase_returns' => [
            'table' => 'purchase_return_lines',
            'parent_column' => 'purchase_return_id',
            'account_type' => 'expense',
            'account_field' => 'expense_account_code',
            'tax_scope' => 'purchase',
            'label' => 'Purchase return line',
        ],
        'expense_returns' => [
            'table' => 'expense_return_lines',
            'parent_column' => 'expense_return_id',
            'account_type' => 'expense',
            'account_field' => 'expense_account_code',
            'tax_scope' => 'purchase',
            'label' => 'Expense return line',
        ],
        'student_invoices' => [
            'table' => 'student_invoice_lines',
            'parent_column' => 'student_invoice_id',
            'account_type' => 'revenue',
            'account_field' => 'revenue_account_code',
            'tax_scope' => 'sales',
            'label' => 'Student invoice line',
        ],
        default => null,
    };
}

function app_document_line_payload_has_rows(array $input): bool
{
    foreach (['doc_line_description', 'doc_line_item_code', 'doc_line_account_code', 'doc_line_quantity', 'doc_line_unit_price'] as $key) {
        if (isset($input[$key]) && is_array($input[$key]) && count($input[$key]) > 0) {
            return true;
        }
    }
    return false;
}

function app_document_line_from_record(string $slug, array $record): array
{
    $config = app_document_line_config($slug);
    if ($config === null) {
        return [];
    }

    $headerSubtotal = round(max(0, app_number($record['subtotal'] ?? 0)), 2);
    $discount = round(max(0, app_number($record['discount_total'] ?? 0)), 2);
    $taxTotal = round(max(0, app_number($record['tax_total'] ?? 0)), 2);
    $grandTotal = round(max(0, app_number($record['grand_total'] ?? 0)), 2);
    if ($headerSubtotal <= 0 && $grandTotal <= 0) {
        return [];
    }

    $lineSubtotal = $grandTotal > 0
        ? round(max(0, $grandTotal - $taxTotal), 2)
        : round(max(0, $headerSubtotal - $discount), 2);
    $lineGross = round(max($lineSubtotal + $discount, $headerSubtotal), 2);
    if ($lineGross <= 0 && $lineSubtotal > 0) {
        $lineGross = $lineSubtotal;
    }
    $lineTotal = $grandTotal > 0 ? $grandTotal : round($lineSubtotal + $taxTotal, 2);

    $description = app_normalize_whitespace((string) ($record['description'] ?? ''));
    if ($description === '') {
        $description = match ($slug) {
            'sales_invoices' => 'Sales invoice',
            'purchase_bills' => 'Purchase bill',
            'expenses' => 'Expense',
            'sales_returns' => 'Sales return',
            'purchase_returns' => 'Purchase return',
            'expense_returns' => 'Expense return',
            'student_invoices' => 'Student fees',
            default => 'Document line',
        };
    }

    return [[
        'line_no' => 1,
        'item_code' => $slug === 'student_invoices' ? app_normalize_whitespace((string) ($record['revenue_type_code'] ?? '')) : '',
        'description' => $description,
        'account_code' => app_normalize_whitespace((string) ($record[(string) $config['account_field']] ?? '')),
        'cost_center_code' => app_normalize_whitespace((string) ($record['cost_center_code'] ?? '')),
        'quantity' => 1.0,
        'unit_price' => $lineGross,
        'discount_total' => $discount,
        'is_taxable' => $taxTotal > 0 ? 1 : (!empty($record['is_taxable']) ? 1 : 0),
        'tax_name' => app_normalize_whitespace((string) ($record['tax_name'] ?? '')),
        'tax_rate_percent' => max(0, app_number($record['tax_rate_percent'] ?? 0)),
        'subtotal' => $lineSubtotal,
        'tax_total' => $taxTotal,
        'line_total' => $lineTotal,
    ]];
}

function app_fetch_document_lines(string $slug, int $companyId, int $documentId, ?array $fallbackRecord = null): array
{
    $config = app_document_line_config($slug);
    if ($config === null || $companyId <= 0 || $documentId <= 0) {
        return $fallbackRecord !== null ? app_document_line_from_record($slug, $fallbackRecord) : [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM `' . $config['table'] . '`
         WHERE company_id = :company_id
           AND `' . $config['parent_column'] . '` = :document_id
         ORDER BY line_no ASC, id ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'document_id' => $documentId]);
    $lines = $stmt->fetchAll() ?: [];
    if (!$lines && $fallbackRecord !== null) {
        return app_document_line_from_record($slug, $fallbackRecord);
    }
    return $lines;
}

function app_default_document_line_account(string $slug, array $data, int $companyId): string
{
    $config = app_document_line_config($slug);
    if ($config === null) {
        return '';
    }
    $accountCode = app_normalize_whitespace((string) ($data[(string) $config['account_field']] ?? ''));
    if ($accountCode !== '') {
        return $accountCode;
    }
    if ($slug === 'student_invoices') {
        $settings = app_student_account_settings($companyId);
        return app_normalize_whitespace((string) ($settings['default_revenue_account_code'] ?? ''));
    }
    return '';
}

function app_enrich_student_document_line(int $companyId, array $line): array
{
    $typeCode = app_normalize_whitespace((string) ($line['item_code'] ?? ''));
    if ($companyId <= 0 || $typeCode === '') {
        return $line;
    }
    $revenueType = app_student_revenue_type_by_code($companyId, $typeCode);
    if (!$revenueType) {
        return $line;
    }

    if (app_normalize_whitespace((string) ($line['description'] ?? '')) === '') {
        $line['description'] = (string) ($revenueType['revenue_type_name'] ?? '');
    }
    if (app_normalize_whitespace((string) ($line['account_code'] ?? '')) === '') {
        $line['account_code'] = (string) ($revenueType['revenue_account_code'] ?? '');
    }
    if (app_normalize_whitespace((string) ($line['cost_center_code'] ?? '')) === '') {
        $line['cost_center_code'] = (string) ($revenueType['cost_center_code'] ?? '');
    }
    if (max(0, app_number($line['unit_price'] ?? 0)) <= 0 && max(0, app_number($revenueType['default_amount'] ?? 0)) > 0) {
        $line['unit_price'] = (float) $revenueType['default_amount'];
    }
    if (empty($line['is_taxable'])) {
        $line['is_taxable'] = (int) ($revenueType['is_taxable'] ?? 0);
    }
    if (app_normalize_whitespace((string) ($line['tax_name'] ?? '')) === '') {
        $line['tax_name'] = (string) ($revenueType['tax_name'] ?? '');
    }
    return $line;
}

function app_parse_document_lines(string $slug, array $input, array $data, ?int $companyId): array
{
    $config = app_document_line_config($slug);
    $companyId = (int) ($companyId ?? 0);
    if ($config === null) {
        return [];
    }

    if (!app_document_line_payload_has_rows($input)) {
        return app_document_line_from_record($slug, $data);
    }

    $items = array_values((array) ($input['doc_line_item_code'] ?? []));
    $descriptions = array_values((array) ($input['doc_line_description'] ?? []));
    $accounts = array_values((array) ($input['doc_line_account_code'] ?? []));
    $costCenters = array_values((array) ($input['doc_line_cost_center_code'] ?? []));
    $quantities = array_values((array) ($input['doc_line_quantity'] ?? []));
    $unitPrices = array_values((array) ($input['doc_line_unit_price'] ?? []));
    $discounts = array_values((array) ($input['doc_line_discount_total'] ?? []));
    $taxableFlags = array_values((array) ($input['doc_line_is_taxable'] ?? []));
    $taxNames = array_values((array) ($input['doc_line_tax_name'] ?? []));
    $taxRates = array_values((array) ($input['doc_line_tax_rate_percent'] ?? []));
    $lineCount = max(count($items), count($descriptions), count($accounts), count($costCenters), count($quantities), count($unitPrices), count($discounts), count($taxableFlags), count($taxNames), count($taxRates));

    $lines = [];
    $defaultAccount = app_default_document_line_account($slug, $data, $companyId);
    $defaultCostCenter = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
    if ($slug === 'student_invoices' && $defaultCostCenter === '' && $companyId > 0) {
        $settings = app_student_account_settings($companyId);
        $defaultCostCenter = app_normalize_whitespace((string) ($settings['default_cost_center_code'] ?? ''));
    }

    for ($index = 0; $index < $lineCount; $index++) {
        $line = [
            'line_no' => count($lines) + 1,
            'item_code' => app_normalize_whitespace((string) ($items[$index] ?? '')),
            'description' => app_normalize_whitespace((string) ($descriptions[$index] ?? '')),
            'account_code' => app_normalize_whitespace((string) ($accounts[$index] ?? $defaultAccount)),
            'cost_center_code' => app_normalize_whitespace((string) ($costCenters[$index] ?? $defaultCostCenter)),
            'quantity' => max(0, app_number($quantities[$index] ?? 0)),
            'unit_price' => max(0, app_number($unitPrices[$index] ?? 0)),
            'discount_total' => max(0, app_number($discounts[$index] ?? 0)),
            'is_taxable' => !empty($taxableFlags[$index]) ? 1 : 0,
            'tax_name' => app_normalize_whitespace((string) ($taxNames[$index] ?? '')),
            'tax_rate_percent' => max(0, app_number($taxRates[$index] ?? 0)),
        ];

        if ($slug === 'student_invoices') {
            $line = app_enrich_student_document_line($companyId, $line);
        }
        if ($line['description'] === '' && $line['item_code'] !== '') {
            $line['description'] = $line['item_code'];
        }

        $hasValues = $line['item_code'] !== ''
            || $line['description'] !== ''
            || $line['account_code'] !== ''
            || $line['cost_center_code'] !== ''
            || $line['quantity'] > 0
            || $line['unit_price'] > 0
            || $line['discount_total'] > 0;
        if (!$hasValues) {
            continue;
        }

        if ($line['quantity'] <= 0) {
            $line['quantity'] = 1.0;
        }
        $gross = round($line['quantity'] * $line['unit_price'], 2);
        if ($line['discount_total'] > $gross) {
            $line['discount_total'] = $gross;
        }
        $base = round(max(0, $gross - $line['discount_total']), 2);

        if ($line['is_taxable'] === 1) {
            $taxRow = null;
            if ($line['tax_name'] !== '' && $companyId > 0) {
                $taxRow = app_tax_rate_row_by_name($companyId, $line['tax_name'], (string) $config['tax_scope']);
            }
            if ($taxRow) {
                $line['tax_rate_percent'] = max(0, app_number($taxRow['rate_percent'] ?? $line['tax_rate_percent']));
            } elseif ($line['tax_rate_percent'] <= 0 && $companyId > 0) {
                $defaultTax = app_default_tax_rate_row($companyId, (string) $config['tax_scope']);
                if ($defaultTax) {
                    $line['tax_name'] = app_normalize_whitespace((string) ($defaultTax['tax_name'] ?? $line['tax_name']));
                    $line['tax_rate_percent'] = max(0, app_number($defaultTax['rate_percent'] ?? 0));
                }
            }
            if ($line['tax_rate_percent'] <= 0) {
                $line['tax_rate_percent'] = 15;
            }
        } else {
            $line['tax_name'] = '';
            $line['tax_rate_percent'] = 0.0;
        }

        $line['subtotal'] = $base;
        $line['tax_total'] = $line['is_taxable'] === 1 ? round($base * ($line['tax_rate_percent'] / 100), 2) : 0.0;
        $line['line_total'] = round($base + $line['tax_total'], 2);
        $lines[] = $line;
    }

    return $lines ?: app_document_line_from_record($slug, $data);
}

function app_apply_document_line_totals(string $slug, array $data, array $lines): array
{
    if (!in_array($slug, app_document_line_slugs(), true)) {
        return $data;
    }

    $subtotal = 0.0;
    $discount = 0.0;
    $taxTotal = 0.0;
    $grandTotal = 0.0;
    $firstTaxName = '';
    $firstTaxRate = 0.0;
    $firstAccount = '';
    $firstCostCenter = '';
    $firstItem = '';
    foreach ($lines as $line) {
        $subtotal += max(0, app_number($line['subtotal'] ?? 0));
        $discount += max(0, app_number($line['discount_total'] ?? 0));
        $taxTotal += max(0, app_number($line['tax_total'] ?? 0));
        $grandTotal += max(0, app_number($line['line_total'] ?? 0));
        $firstAccount = $firstAccount !== '' ? $firstAccount : app_normalize_whitespace((string) ($line['account_code'] ?? ''));
        $firstCostCenter = $firstCostCenter !== '' ? $firstCostCenter : app_normalize_whitespace((string) ($line['cost_center_code'] ?? ''));
        $firstItem = $firstItem !== '' ? $firstItem : app_normalize_whitespace((string) ($line['item_code'] ?? ''));
        if ($firstTaxName === '' && !empty($line['is_taxable'])) {
            $firstTaxName = app_normalize_whitespace((string) ($line['tax_name'] ?? ''));
            $firstTaxRate = max(0, app_number($line['tax_rate_percent'] ?? 0));
        }
    }

    $config = app_document_line_config($slug);
    if ($config !== null && $firstAccount !== '' && app_normalize_whitespace((string) ($data[(string) $config['account_field']] ?? '')) === '') {
        $data[(string) $config['account_field']] = $firstAccount;
    }
    if ($firstCostCenter !== '' && app_normalize_whitespace((string) ($data['cost_center_code'] ?? '')) === '') {
        $data['cost_center_code'] = $firstCostCenter;
    }
    if ($slug === 'student_invoices' && $firstItem !== '' && app_normalize_whitespace((string) ($data['revenue_type_code'] ?? '')) === '') {
        $data['revenue_type_code'] = $firstItem;
    }

    $data['subtotal'] = round($subtotal, 2);
    $data['discount_total'] = round($discount, 2);
    $data['tax_total'] = round($taxTotal, 2);
    $data['grand_total'] = round($grandTotal, 2);
    $data['is_taxable'] = $taxTotal > 0 ? 1 : 0;
    $data['tax_name'] = $taxTotal > 0 ? $firstTaxName : '';
    $data['tax_rate_percent'] = $taxTotal > 0 ? $firstTaxRate : 0.0;

    $inlinePaidAmount = max(0, app_number($data['inline_paid_amount'] ?? ($data['paid_amount'] ?? 0)));
    if ($inlinePaidAmount > $data['grand_total']) {
        $inlinePaidAmount = $data['grand_total'];
    }
    if ($inlinePaidAmount <= 0) {
        $inlinePaidAmount = 0.0;
        $data['payment_method'] = '';
        $data['payment_account_name'] = '';
    }
    $data['inline_paid_amount'] = round($inlinePaidAmount, 2);
    $data['paid_amount'] = round($inlinePaidAmount, 2);
    $data['status'] = app_document_payment_status($slug, (float) $data['grand_total'], (float) $inlinePaidAmount, (string) ($data['status'] ?? ''));

    if ($slug === 'expenses') {
        $remainingAmount = max(0, round((float) $data['grand_total'] - $inlinePaidAmount, 2));
        if ($remainingAmount <= 0.01) {
            $data['offset_account_code'] = '';
        }
    }

    return $data;
}

function app_validate_document_lines(string $slug, int $companyId, array $lines): void
{
    $config = app_document_line_config($slug);
    if ($config === null) {
        return;
    }
    if (!$lines) {
        throw new RuntimeException('Add at least one document line.');
    }

    foreach ($lines as $index => $line) {
        $lineNo = $index + 1;
        if (max(0, app_number($line['line_total'] ?? 0)) <= 0) {
            throw new RuntimeException('Line ' . $lineNo . ': amount must be greater than zero.');
        }
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($line['account_code'] ?? '')), (string) $config['account_type'], 'Line ' . $lineNo . ' account');

        $costCenterCode = app_normalize_whitespace((string) ($line['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Line ' . $lineNo . ': cost center is required for revenue/expense posting.');
        }

        if ($slug === 'student_invoices') {
            $itemCode = app_normalize_whitespace((string) ($line['item_code'] ?? ''));
            if ($itemCode !== '' && !app_student_revenue_type_by_code($companyId, $itemCode)) {
                throw new RuntimeException('Line ' . $lineNo . ': select a valid student revenue type.');
            }
        }

        if (!empty($line['is_taxable'])) {
            if (max(0, app_number($line['tax_rate_percent'] ?? 0)) <= 0) {
                throw new RuntimeException('Line ' . $lineNo . ': tax rate is required when taxable is selected.');
            }
            $taxName = app_normalize_whitespace((string) ($line['tax_name'] ?? ''));
            if ($taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, (string) $config['tax_scope'])) {
                throw new RuntimeException('Line ' . $lineNo . ': select a valid tax code.');
            }
        }
    }
}

function app_replace_document_lines(string $slug, int $companyId, int $documentId, array $lines): void
{
    $config = app_document_line_config($slug);
    if ($config === null || $companyId <= 0 || $documentId <= 0) {
        return;
    }

    $pdo = app_pdo();
    $pdo->prepare(
        'DELETE FROM `' . $config['table'] . '`
         WHERE company_id = :company_id
           AND `' . $config['parent_column'] . '` = :document_id'
    )->execute(['company_id' => $companyId, 'document_id' => $documentId]);

    $stmt = $pdo->prepare(
        'INSERT INTO `' . $config['table'] . '`
         (company_id, `' . $config['parent_column'] . '`, line_no, item_code, description, account_code, cost_center_code, quantity, unit_price, discount_total, is_taxable, tax_name, tax_rate_percent, subtotal, tax_total, line_total)
         VALUES
         (:company_id, :document_id, :line_no, :item_code, :description, :account_code, :cost_center_code, :quantity, :unit_price, :discount_total, :is_taxable, :tax_name, :tax_rate_percent, :subtotal, :tax_total, :line_total)'
    );

    $lineNo = 1;
    foreach ($lines as $line) {
        $stmt->execute([
            'company_id' => $companyId,
            'document_id' => $documentId,
            'line_no' => $lineNo++,
            'item_code' => app_normalize_whitespace((string) ($line['item_code'] ?? '')),
            'description' => app_normalize_whitespace((string) ($line['description'] ?? '')),
            'account_code' => app_normalize_whitespace((string) ($line['account_code'] ?? '')),
            'cost_center_code' => app_normalize_whitespace((string) ($line['cost_center_code'] ?? '')),
            'quantity' => round(max(0, app_number($line['quantity'] ?? 0)), 4),
            'unit_price' => round(max(0, app_number($line['unit_price'] ?? 0)), 2),
            'discount_total' => round(max(0, app_number($line['discount_total'] ?? 0)), 2),
            'is_taxable' => !empty($line['is_taxable']) ? 1 : 0,
            'tax_name' => app_normalize_whitespace((string) ($line['tax_name'] ?? '')),
            'tax_rate_percent' => round(max(0, app_number($line['tax_rate_percent'] ?? 0)), 2),
            'subtotal' => round(max(0, app_number($line['subtotal'] ?? 0)), 2),
            'tax_total' => round(max(0, app_number($line['tax_total'] ?? 0)), 2),
            'line_total' => round(max(0, app_number($line['line_total'] ?? 0)), 2),
        ]);
    }
}

function app_normalize_student_receipt_payload(array $data, ?int $companyId): array
{
    $companyId = (int) ($companyId ?? 0);
    $invoiceId = app_integer($data['student_invoice_id'] ?? 0);
    if ($companyId > 0 && $invoiceId > 0) {
        $invoice = app_student_invoice_by_id($companyId, $invoiceId);
        if ($invoice) {
            if (app_normalize_whitespace((string) ($data['student_number'] ?? '')) === '') {
                $data['student_number'] = (string) ($invoice['student_number'] ?? '');
            }
            if (app_normalize_whitespace((string) ($data['guardian_contact_code'] ?? '')) === '') {
                $data['guardian_contact_code'] = (string) ($invoice['guardian_contact_code'] ?? '');
            }
            if (app_normalize_whitespace((string) ($data['reference'] ?? '')) === '') {
                $data['reference'] = (string) ($invoice['student_invoice_number'] ?? '');
            }
        }
    }
    $data['receipt_date'] = app_sanitize_date((string) ($data['receipt_date'] ?? ''), date('Y-m-d'));
    $data['amount'] = round(max(0, app_number($data['amount'] ?? 0)), 2);
    return $data;
}

function app_normalize_student_refund_payload(array $data, ?int $companyId): array
{
    $companyId = (int) ($companyId ?? 0);
    $student = $companyId > 0 ? app_student_by_number($companyId, (string) ($data['student_number'] ?? '')) : null;
    if ($student && app_normalize_whitespace((string) ($data['guardian_contact_code'] ?? '')) === '') {
        $data['guardian_contact_code'] = (string) ($student['guardian_contact_code'] ?? '');
    }
    if ($companyId > 0 && app_normalize_whitespace((string) ($data['offset_account_code'] ?? '')) === '') {
        $settings = app_student_account_settings($companyId);
        $data['offset_account_code'] = (string) ($settings['discount_account_code'] ?? '');
    }
    $data['voucher_date'] = app_sanitize_date((string) ($data['voucher_date'] ?? ''), date('Y-m-d'));
    $data['amount'] = round(max(0, app_number($data['amount'] ?? 0)), 2);
    return $data;
}

function app_excellence_operation_by_number(int $companyId, string $operationNumber): ?array
{
    $operationNumber = app_normalize_whitespace($operationNumber);
    if ($companyId <= 0 || $operationNumber === '') {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM excellence_operations
         WHERE company_id = :company_id
           AND operation_number = :operation_number
         LIMIT 1'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'operation_number' => $operationNumber,
    ]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_normalize_excellence_payload(string $slug, array $data): array
{
    if ($slug === 'excellence_operations') {
        $data['operation_date'] = app_sanitize_date((string) ($data['operation_date'] ?? ''), date('Y-m-d'));
        $quantity = max(0, app_number($data['quantity'] ?? 1));
        if ($quantity <= 0) {
            $quantity = 1;
        }
        $rate = max(0, app_number($data['rental_rate'] ?? 0));
        $revenue = max(0, app_number($data['revenue_amount'] ?? 0));
        if ($revenue <= 0 && $rate > 0) {
            $revenue = round($quantity * $rate, 2);
        }
        $hospitalExpense = max(0, app_number($data['hospital_expense_amount'] ?? 0));
        $data['quantity'] = $quantity;
        $data['rental_rate'] = round($rate, 2);
        $data['revenue_amount'] = round($revenue, 2);
        $data['hospital_expense_amount'] = round($hospitalExpense, 2);
        $data['net_operation_income'] = round($revenue - $hospitalExpense, 2);
        $data['status'] = app_normalize_whitespace((string) ($data['status'] ?? 'draft')) ?: 'draft';
    }

    if ($slug === 'excellence_hospital_expenses') {
        $data['expense_date'] = app_sanitize_date((string) ($data['expense_date'] ?? ''), date('Y-m-d'));
        $data['amount'] = round(max(0, app_number($data['amount'] ?? 0)), 2);
        $data['settlement_mode'] = app_normalize_whitespace((string) ($data['settlement_mode'] ?? 'accrued_operation')) ?: 'accrued_operation';
        $data['status'] = app_normalize_whitespace((string) ($data['status'] ?? 'draft')) ?: 'draft';
    }

    if ($slug === 'excellence_admin_expenses') {
        $data['expense_date'] = app_sanitize_date((string) ($data['expense_date'] ?? ''), date('Y-m-d'));
        $data['amount'] = round(max(0, app_number($data['amount'] ?? 0)), 2);
        $data['status'] = app_normalize_whitespace((string) ($data['status'] ?? 'draft')) ?: 'draft';
    }

    if ($slug === 'excellence_salaries') {
        $data['salary_date'] = app_sanitize_date((string) ($data['salary_date'] ?? ''), date('Y-m-d'));
        $data['amount'] = round(max(0, app_number($data['amount'] ?? 0)), 2);
        $data['status'] = app_normalize_whitespace((string) ($data['status'] ?? 'draft')) ?: 'draft';
    }

    return $data;
}

function app_validate_transaction_master_links(string $slug, array $data, ?int $companyId): void
{
    if ($companyId === null || $companyId <= 0) {
        return;
    }

    if ($slug === 'chart_of_accounts') {
        $accountCode = app_normalize_whitespace((string) ($data['account_code'] ?? ''));
        $accountType = strtolower(app_normalize_whitespace((string) ($data['account_type'] ?? '')));
        $openingBalance = app_number($data['opening_balance'] ?? 0);
        if ($accountCode !== '' && abs($openingBalance) > 0.0001 && app_account_has_children($companyId, $accountCode)) {
            throw new RuntimeException('Opening balances are allowed on sub accounts only. Account "' . $accountCode . '" is a parent account.');
        }
        $statementGroupCode = app_normalize_whitespace((string) ($data['financial_statement_group_code'] ?? ''));
        if ($statementGroupCode !== '') {
            $statementGroup = app_financial_statement_group_by_code($companyId, $statementGroupCode);
            if (!$statementGroup) {
                throw new RuntimeException('Select a valid financial statement group.');
            }
            $mainSection = app_normalize_whitespace((string) ($statementGroup['main_section'] ?? ''));
            $section = app_financial_statement_main_sections()[$mainSection] ?? null;
            $allowedTypes = array_map('strtolower', (array) ($section['account_types'] ?? []));
            if ($accountType !== '' && $allowedTypes && !in_array($accountType, $allowedTypes, true)) {
                throw new RuntimeException('Selected financial statement group is not compatible with this account type.');
            }
        }
    }

    if ($slug === 'financial_statement_groups') {
        $statementType = app_normalize_whitespace((string) ($data['statement_type'] ?? ''));
        $mainSection = app_normalize_whitespace((string) ($data['main_section'] ?? ''));
        $section = app_financial_statement_main_sections()[$mainSection] ?? null;
        if (!$section || !in_array($statementType, ['balance_sheet', 'income_statement'], true)) {
            throw new RuntimeException('Select a valid financial statement section.');
        }
        if ((string) ($section['statement_type'] ?? '') !== $statementType) {
            throw new RuntimeException('Main section does not belong to the selected statement type.');
        }
    }

    if ($slug === 'bank_accounts') {
        $glAccountCode = app_normalize_whitespace((string) ($data['gl_account_code'] ?? ''));
        if ($glAccountCode !== '') {
            app_assert_leaf_account($companyId, $glAccountCode, 'asset', 'Bank GL account');
        }
    }

    if ($slug === 'cash_accounts') {
        $glAccountCode = app_normalize_whitespace((string) ($data['gl_account_code'] ?? ''));
        if ($glAccountCode !== '') {
            app_assert_leaf_account($companyId, $glAccountCode, 'asset', 'Cash GL account');
        }
    }

    if ($slug === 'hr_departments') {
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode !== '' && !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Select a valid cost center for the HR department.');
        }
    }

    if ($slug === 'hr_job_titles') {
        $departmentCode = app_normalize_whitespace((string) ($data['department_code'] ?? ''));
        if ($departmentCode !== '' && !array_key_exists($departmentCode, app_module_lookup_options('hr_department_code', $companyId))) {
            throw new RuntimeException('Select a valid department for the job title.');
        }
    }

    if ($slug === 'hr_salary_components') {
        $accountCode = app_normalize_whitespace((string) ($data['account_code'] ?? ''));
        if ($accountCode !== '') {
            app_assert_leaf_account($companyId, $accountCode, null, 'Salary component account');
        }
    }

    if (in_array($slug, ['employees', 'hr_attendance', 'hr_leave_requests', 'hr_employee_contracts', 'hr_payroll_items'], true)) {
        $employeeNumber = app_normalize_whitespace((string) ($data['employee_number'] ?? ''));
        if (in_array($slug, ['hr_attendance', 'hr_leave_requests', 'hr_employee_contracts', 'hr_payroll_items'], true) && ($employeeNumber === '' || !array_key_exists($employeeNumber, app_module_lookup_options('employee_number', $companyId)))) {
            throw new RuntimeException('Select a valid employee.');
        }
        if ($slug === 'employees') {
            $departmentCode = app_normalize_whitespace((string) ($data['department_code'] ?? ''));
            if ($departmentCode !== '' && !array_key_exists($departmentCode, app_module_lookup_options('hr_department_code', $companyId))) {
                throw new RuntimeException('Select a valid employee department.');
            }
            $jobCode = app_normalize_whitespace((string) ($data['job_code'] ?? ''));
            if ($jobCode !== '' && !array_key_exists($jobCode, app_module_lookup_options('hr_job_code', $companyId))) {
                throw new RuntimeException('Select a valid employee job title.');
            }
        }
        if ($slug === 'hr_attendance') {
            $shiftCode = app_normalize_whitespace((string) ($data['shift_code'] ?? ''));
            if ($shiftCode !== '' && !array_key_exists($shiftCode, app_module_lookup_options('hr_shift_code', $companyId))) {
                throw new RuntimeException('Select a valid shift.');
            }
        }
        if ($slug === 'hr_leave_requests') {
            $leaveTypeCode = app_normalize_whitespace((string) ($data['leave_type_code'] ?? ''));
            if ($leaveTypeCode !== '' && !array_key_exists($leaveTypeCode, app_module_lookup_options('hr_leave_type_code', $companyId))) {
                throw new RuntimeException('Select a valid leave type.');
            }
        }
        if ($slug === 'hr_employee_contracts') {
            $departmentCode = app_normalize_whitespace((string) ($data['department_code'] ?? ''));
            if ($departmentCode !== '' && !array_key_exists($departmentCode, app_module_lookup_options('hr_department_code', $companyId))) {
                throw new RuntimeException('Select a valid contract department.');
            }
            $jobCode = app_normalize_whitespace((string) ($data['job_code'] ?? ''));
            if ($jobCode !== '' && !array_key_exists($jobCode, app_module_lookup_options('hr_job_code', $companyId))) {
                throw new RuntimeException('Select a valid contract job title.');
            }
        }
        if ($slug === 'hr_payroll_items') {
            $runNumber = app_normalize_whitespace((string) ($data['run_number'] ?? ''));
            if ($runNumber !== '' && !array_key_exists($runNumber, app_module_lookup_options('payroll_run_number', $companyId))) {
                throw new RuntimeException('Select a valid payroll run.');
            }
            $componentCode = app_normalize_whitespace((string) ($data['component_code'] ?? ''));
            if ($componentCode !== '' && !array_key_exists($componentCode, app_module_lookup_options('hr_salary_component_code', $companyId))) {
                throw new RuntimeException('Select a valid salary component.');
            }
            $accountCode = app_normalize_whitespace((string) ($data['account_code'] ?? ''));
            if ($accountCode !== '') {
                app_assert_leaf_account($companyId, $accountCode, null, 'Payroll item account');
            }
            $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
            if ($costCenterCode !== '' && !app_cost_center_exists($companyId, $costCenterCode)) {
                throw new RuntimeException('Select a valid payroll cost center.');
            }
        }
    }

    if ($slug === 'student_account_settings') {
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['receivable_account_code'] ?? '')), 'asset', 'Student receivable account');
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['advance_account_code'] ?? '')), 'liability', 'Student advance account');
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['discount_account_code'] ?? '')), null, 'Student refund/discount account');
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['default_revenue_account_code'] ?? '')), 'revenue', 'Default student revenue account');
        $defaultCostCenter = app_normalize_whitespace((string) ($data['default_cost_center_code'] ?? ''));
        if ($defaultCostCenter === '' || !app_cost_center_exists($companyId, $defaultCostCenter)) {
            throw new RuntimeException('Default student cost center is required.');
        }
    }

    if (in_array($slug, ['student_grades', 'student_classes', 'students', 'student_fee_plans', 'student_invoices'], true)) {
        $stageCode = app_normalize_whitespace((string) ($data['stage_code'] ?? ''));
        if ($stageCode !== '' && !app_student_stage_exists($companyId, $stageCode)) {
            throw new RuntimeException('Select a valid student stage.');
        }

        $gradeCode = app_normalize_whitespace((string) ($data['grade_code'] ?? ''));
        if ($gradeCode !== '') {
            $grade = app_student_grade_row($companyId, $gradeCode);
            if (!$grade) {
                throw new RuntimeException('Select a valid grade.');
            }
            $gradeStage = app_normalize_whitespace((string) ($grade['stage_code'] ?? ''));
            if ($stageCode !== '' && $gradeStage !== '' && $gradeStage !== $stageCode) {
                throw new RuntimeException('Selected grade does not belong to the selected stage.');
            }
        }

        $classCode = app_normalize_whitespace((string) ($data['class_code'] ?? ''));
        if ($classCode !== '') {
            $class = app_student_class_row($companyId, $classCode);
            if (!$class) {
                throw new RuntimeException('Select a valid class.');
            }
            $classStage = app_normalize_whitespace((string) ($class['stage_code'] ?? ''));
            $classGrade = app_normalize_whitespace((string) ($class['grade_code'] ?? ''));
            if ($stageCode !== '' && $classStage !== '' && $classStage !== $stageCode) {
                throw new RuntimeException('Selected class does not belong to the selected stage.');
            }
            if ($gradeCode !== '' && $classGrade !== '' && $classGrade !== $gradeCode) {
                throw new RuntimeException('Selected class does not belong to the selected grade.');
            }
        }
    }

    if ($slug === 'students') {
        $guardianCode = app_normalize_whitespace((string) ($data['guardian_contact_code'] ?? ''));
        if ($guardianCode !== '' && !app_contact_exists($companyId, $guardianCode, 'customer')) {
            throw new RuntimeException('Select a valid guardian/customer contact.');
        }
    }

    if ($slug === 'student_revenue_types') {
        $revenueCode = app_normalize_whitespace((string) ($data['revenue_account_code'] ?? ''));
        if ($revenueCode !== '') {
            app_assert_leaf_account($companyId, $revenueCode, 'revenue', 'Student revenue account');
        }
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode !== '' && !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Select a valid cost center for this revenue type.');
        }
        $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
        if (!empty($data['is_taxable']) && $taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, 'sales')) {
            throw new RuntimeException('Select a valid sales tax code for this revenue type.');
        }
    }

    if ($slug === 'student_fee_plans') {
        $revenueTypeCode = app_normalize_whitespace((string) ($data['revenue_type_code'] ?? ''));
        if ($revenueTypeCode === '' || !app_student_revenue_type_by_code($companyId, $revenueTypeCode)) {
            throw new RuntimeException('Select a valid student revenue type.');
        }
        $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
        if (!empty($data['is_taxable']) && $taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, 'sales')) {
            throw new RuntimeException('Select a valid sales tax code for this fee plan.');
        }
    }

    if ($slug === 'student_invoices') {
        $studentNumber = app_normalize_whitespace((string) ($data['student_number'] ?? ''));
        if ($studentNumber === '' || !app_student_by_number($companyId, $studentNumber)) {
            throw new RuntimeException('Select a valid student.');
        }
        $guardianCode = app_normalize_whitespace((string) ($data['guardian_contact_code'] ?? ''));
        if ($guardianCode === '' || !app_contact_exists($companyId, $guardianCode, 'customer')) {
            throw new RuntimeException('Select a valid guardian/customer contact.');
        }
        $revenueTypeCode = app_normalize_whitespace((string) ($data['revenue_type_code'] ?? ''));
        if ($revenueTypeCode === '' || !app_student_revenue_type_by_code($companyId, $revenueTypeCode)) {
            throw new RuntimeException('Select a valid student revenue type.');
        }
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['revenue_account_code'] ?? '')), 'revenue', 'Student invoice revenue account');
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for student invoice posting.');
        }
        $isTaxable = !empty($data['is_taxable']) ? 1 : 0;
        $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
        $taxRate = max(0, app_number($data['tax_rate_percent'] ?? 0));
        if ($isTaxable === 1 && $taxRate <= 0) {
            throw new RuntimeException('Tax rate must be greater than zero when student invoice is taxable.');
        }
        if ($isTaxable === 1 && $taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, 'sales')) {
            throw new RuntimeException('Select a valid sales tax code for the student invoice.');
        }
        $inlinePaidAmount = max(0, app_number($data['inline_paid_amount'] ?? 0));
        if ($inlinePaidAmount > 0) {
            $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
            if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
                throw new RuntimeException('Select a valid payment method for collected amount.');
            }
            app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Collection account', $paymentMethod);
        }
    }

    if ($slug === 'student_receipts') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Receipt amount must be greater than zero.');
        }
        $studentNumber = app_normalize_whitespace((string) ($data['student_number'] ?? ''));
        if ($studentNumber === '' || !app_student_by_number($companyId, $studentNumber)) {
            throw new RuntimeException('Select a valid student for the receipt.');
        }
        $invoiceId = app_integer($data['student_invoice_id'] ?? 0);
        if ($invoiceId > 0) {
            $invoice = app_student_invoice_by_id($companyId, $invoiceId);
            if (!$invoice) {
                throw new RuntimeException('Selected student invoice does not exist.');
            }
            $status = strtolower(app_normalize_whitespace((string) ($invoice['status'] ?? '')));
            if (in_array($status, ['draft', 'cancelled'], true)) {
                throw new RuntimeException('Student invoice must be posted before registering a receipt.');
            }
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method for the receipt.');
        }
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Receipt account', $paymentMethod);
    }

    if ($slug === 'student_refunds') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Payment voucher amount must be greater than zero.');
        }
        $studentNumber = app_normalize_whitespace((string) ($data['student_number'] ?? ''));
        if ($studentNumber === '' || !app_student_by_number($companyId, $studentNumber)) {
            throw new RuntimeException('Select a valid student for the payment voucher.');
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method for the payment voucher.');
        }
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Voucher payment account', $paymentMethod);
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['offset_account_code'] ?? '')), null, 'Voucher offset account');
    }

    if ($slug === 'excellence_devices') {
        $revenueCode = app_normalize_whitespace((string) ($data['revenue_account_code'] ?? ''));
        if ($revenueCode !== '') {
            app_assert_leaf_account($companyId, $revenueCode, 'revenue', 'Device revenue account');
        }
        $hospitalExpenseCode = app_normalize_whitespace((string) ($data['hospital_expense_account_code'] ?? ''));
        if ($hospitalExpenseCode !== '') {
            app_assert_leaf_account($companyId, $hospitalExpenseCode, 'expense', 'Device hospital expense account');
        }
    }

    if ($slug === 'excellence_operations') {
        $hospitalCode = app_normalize_whitespace((string) ($data['hospital_code'] ?? ''));
        if ($hospitalCode === '' || !array_key_exists($hospitalCode, app_module_lookup_options('excellence_hospital_code', $companyId))) {
            throw new RuntimeException('Select a valid hospital.');
        }
        $deviceCode = app_normalize_whitespace((string) ($data['device_code'] ?? ''));
        if ($deviceCode === '' || !array_key_exists($deviceCode, app_module_lookup_options('excellence_device_code', $companyId))) {
            throw new RuntimeException('Select a valid medical device.');
        }
        $revenueAmount = max(0, app_number($data['revenue_amount'] ?? 0));
        $hospitalExpenseAmount = max(0, app_number($data['hospital_expense_amount'] ?? 0));
        if ($revenueAmount <= 0 && $hospitalExpenseAmount <= 0) {
            throw new RuntimeException('Operation revenue or hospital expense amount is required.');
        }
        if ($revenueAmount > 0) {
            app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['revenue_account_code'] ?? '')), 'revenue', 'Operation revenue account');
        }
        if ($hospitalExpenseAmount > 0) {
            app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['hospital_expense_account_code'] ?? '')), 'expense', 'Hospital expense account');
        }
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for EXcellence operations.');
        }
    }

    if ($slug === 'excellence_hospital_expenses') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Hospital expense amount must be greater than zero.');
        }
        $hospitalCode = app_normalize_whitespace((string) ($data['hospital_code'] ?? ''));
        if ($hospitalCode === '' || !array_key_exists($hospitalCode, app_module_lookup_options('excellence_hospital_code', $companyId))) {
            throw new RuntimeException('Select a valid hospital.');
        }
        $operationNumber = app_normalize_whitespace((string) ($data['operation_number'] ?? ''));
        if ($operationNumber !== '' && !app_excellence_operation_by_number($companyId, $operationNumber)) {
            throw new RuntimeException('Select a valid EXcellence operation.');
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method for hospital expense.');
        }
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Hospital expense payment account', $paymentMethod);
        $settlementMode = app_normalize_whitespace((string) ($data['settlement_mode'] ?? 'accrued_operation'));
        if ($settlementMode === 'direct_expense') {
            app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['expense_account_code'] ?? '')), 'expense', 'Direct hospital expense account');
            $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
            if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
                throw new RuntimeException('Cost center is required for direct hospital expenses.');
            }
        } else {
            $offsetCode = app_normalize_whitespace((string) ($data['offset_account_code'] ?? ''));
            if ($offsetCode !== '') {
                app_assert_leaf_account($companyId, $offsetCode, 'liability', 'Hospital accrued expense account');
            }
        }
    }

    if ($slug === 'excellence_admin_expenses') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Administrative expense amount must be greater than zero.');
        }
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['expense_account_code'] ?? '')), 'expense', 'Administrative expense account');
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for administrative expenses.');
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method for administrative expense.');
        }
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Administrative expense payment account', $paymentMethod);
    }

    if ($slug === 'excellence_salaries') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Salary amount must be greater than zero.');
        }
        app_assert_leaf_account($companyId, app_normalize_whitespace((string) ($data['salary_expense_account_code'] ?? '')), 'expense', 'Salary expense account');
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for EXcellence salaries.');
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method for salaries.');
        }
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['payment_account_name'] ?? '')), 'Salary payment account', $paymentMethod);
    }

    if ($slug === 'tax_rates') {
        $taxAccountCode = app_normalize_whitespace((string) ($data['tax_account_code'] ?? ''));
        if ($taxAccountCode !== '') {
            app_assert_leaf_account($companyId, $taxAccountCode, null, 'Tax posting account');
        }
    }

    if ($slug === 'posting_account_settings') {
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['sales_receivable_account_code'] ?? '')),
            'asset',
            'Sales receivable account'
        );
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['purchase_payable_account_code'] ?? '')),
            'liability',
            'Purchase payable account'
        );
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['accrued_expense_account_code'] ?? '')),
            'liability',
            'Accrued expense account'
        );
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['prepaid_expense_account_code'] ?? '')),
            'asset',
            'Prepaid expense account'
        );
        foreach ([
            'ifrs16_rou_asset_account_code' => ['asset', 'IFRS16 ROU asset account'],
            'ifrs16_lease_liability_account_code' => ['liability', 'IFRS16 lease liability account'],
            'ifrs16_interest_expense_account_code' => ['expense', 'IFRS16 interest expense account'],
            'ifrs16_depreciation_expense_account_code' => ['expense', 'IFRS16 depreciation expense account'],
            'ifrs16_accumulated_depreciation_account_code' => ['asset', 'IFRS16 accumulated depreciation account'],
            'ifrs16_restoration_provision_account_code' => ['liability', 'IFRS16 restoration provision account'],
            'ifrs16_lease_incentive_account_code' => ['asset', 'IFRS16 lease incentive account'],
            'pos_sales_revenue_account_code' => ['revenue', 'POS sales revenue account'],
            'pos_cash_over_short_gain_account_code' => ['revenue', 'POS cash overage gain account'],
            'pos_cash_over_short_loss_account_code' => ['expense', 'POS cash shortage loss account'],
        ] as $field => [$expectedType, $label]) {
            app_assert_leaf_account(
                $companyId,
                app_normalize_whitespace((string) ($data[$field] ?? '')),
                $expectedType,
                $label
            );
        }
    }

    if ($slug === 'pos_registers') {
        $data['register_code'] = strtoupper(app_normalize_whitespace((string) ($data['register_code'] ?? '')));
        app_assert_payment_account_postable($companyId, app_normalize_whitespace((string) ($data['cash_account_name'] ?? '')), 'POS cash drawer', 'cash');
        $bankName = app_normalize_whitespace((string) ($data['default_bank_account_name'] ?? ''));
        if ($bankName !== '') {
            app_assert_payment_account_postable($companyId, $bankName, 'POS bank/card account', 'bank');
        }
        $customerCode = app_normalize_whitespace((string) ($data['default_customer_code'] ?? ''));
        if ($customerCode !== '' && !app_contact_exists($companyId, $customerCode, 'customer')) {
            throw new RuntimeException('Select a valid default POS customer.');
        }
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for POS register posting.');
        }
        $salesCode = app_normalize_whitespace((string) ($data['sales_revenue_account_code'] ?? ''));
        if ($salesCode !== '') {
            app_assert_leaf_account($companyId, $salesCode, 'revenue', 'POS register sales revenue account');
        }
    }

    if (in_array($slug, ['sales_invoices', 'sales_returns'], true)) {
        $customerCode = app_normalize_whitespace((string) ($data['customer_code'] ?? ''));
        if ($customerCode === '' || !app_contact_exists($companyId, $customerCode, 'customer')) {
            throw new RuntimeException('Select a valid customer from Contacts.');
        }
        $revenueCode = app_normalize_whitespace((string) ($data['revenue_account_code'] ?? ''));
        if ($revenueCode !== '') {
            app_assert_leaf_account($companyId, $revenueCode, 'revenue', 'Revenue account');
        }
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for sales posting.');
        }

        $isTaxable = !empty($data['is_taxable']) ? 1 : 0;
        $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
        $taxRate = max(0, app_number($data['tax_rate_percent'] ?? 0));
        if ($isTaxable === 1 && $taxRate <= 0) {
            throw new RuntimeException('Tax rate must be greater than zero when invoice is taxable.');
        }
        if ($isTaxable === 1 && $taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, 'sales')) {
            throw new RuntimeException('Select a valid sales tax code.');
        }

        $inlinePaidAmount = max(0, app_number($data['inline_paid_amount'] ?? 0));
        if ($inlinePaidAmount > 0) {
            $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
            if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
                throw new RuntimeException('Select a valid payment method for collected amount.');
            }
            $paymentAccountName = app_normalize_whitespace((string) ($data['payment_account_name'] ?? ''));
            app_assert_payment_account_postable($companyId, $paymentAccountName, 'Collection account', $paymentMethod);
        }
    }

    if (in_array($slug, ['purchase_bills', 'purchase_returns', 'expenses', 'expense_returns'], true)) {
        if (in_array($slug, ['purchase_bills', 'purchase_returns'], true)) {
            $vendorCode = app_normalize_whitespace((string) ($data['vendor_code'] ?? ''));
            if ($vendorCode === '') {
                throw new RuntimeException('Select a valid vendor from Contacts.');
            }
            if (!app_contact_exists($companyId, $vendorCode, 'vendor')) {
                throw new RuntimeException('Select a valid vendor/payee from Contacts.');
            }
            $withholdingTaxName = app_normalize_whitespace((string) ($data['withholding_tax_name'] ?? ''));
            if ($slug === 'purchase_bills' && $withholdingTaxName !== '' && !app_tax_rate_row_by_name($companyId, $withholdingTaxName, 'withholding')) {
                throw new RuntimeException('Select a valid withholding / commercial profit tax code.');
            }
        }

        $expenseCode = app_normalize_whitespace((string) ($data['expense_account_code'] ?? ''));
        if ($expenseCode !== '') {
            app_assert_leaf_account($companyId, $expenseCode, 'expense', 'Expense account');
        }
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for purchase/expense posting.');
        }

        $isTaxable = !empty($data['is_taxable']) ? 1 : 0;
        $taxName = app_normalize_whitespace((string) ($data['tax_name'] ?? ''));
        $taxRate = max(0, app_number($data['tax_rate_percent'] ?? 0));
        if ($isTaxable === 1 && $taxRate <= 0) {
            throw new RuntimeException('Tax rate must be greater than zero when document is taxable.');
        }
        if ($isTaxable === 1 && $taxName !== '' && !app_tax_rate_row_by_name($companyId, $taxName, 'purchase')) {
            throw new RuntimeException('Select a valid purchase tax code.');
        }

        $inlinePaidAmount = max(0, app_number($data['inline_paid_amount'] ?? 0));
        $grandTotal = max(0, app_number($data['grand_total'] ?? 0));
        $paymentBasisTotal = $slug === 'purchase_bills'
            ? max(0, app_number($data['net_payable'] ?? $grandTotal))
            : $grandTotal;
        if ($paymentBasisTotal <= 0 && $grandTotal > 0) {
            $paymentBasisTotal = $grandTotal;
        }
        if ($inlinePaidAmount > $paymentBasisTotal) {
            $inlinePaidAmount = $paymentBasisTotal;
        }
        if (in_array($slug, ['expenses', 'expense_returns'], true)) {
            $remainingAmount = max(0, round($grandTotal - $inlinePaidAmount, 2));
            $offsetCode = app_normalize_whitespace((string) ($data['offset_account_code'] ?? ''));
            if ($remainingAmount > 0.01) {
                if ($offsetCode !== '') {
                    app_assert_leaf_account($companyId, $offsetCode, 'liability', 'Offset account');
                } else {
                    app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
                }
            }
        }
        if ($inlinePaidAmount > 0) {
            $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
            if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
                throw new RuntimeException('Select a valid payment method for paid amount.');
            }
            $paymentAccountName = app_normalize_whitespace((string) ($data['payment_account_name'] ?? ''));
            app_assert_payment_account_postable($companyId, $paymentAccountName, 'Payment account', $paymentMethod);
        }
    }

    if ($slug === 'payments') {
        $amount = max(0, app_number($data['amount'] ?? 0));
        if ($amount <= 0) {
            throw new RuntimeException('Payment amount must be greater than zero.');
        }
        $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
        if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
            throw new RuntimeException('Select a valid payment method.');
        }

        $salesInvoiceId = app_integer($data['sales_invoice_id'] ?? 0);
        $purchaseBillId = app_integer($data['purchase_bill_id'] ?? 0);
        $expenseId = app_integer($data['expense_id'] ?? 0);
        $currentPaymentId = app_integer($data['__record_id'] ?? 0);
        $ifrs16LeaseId = app_integer($data['ifrs16_lease_id'] ?? 0);
        $ifrs16ScheduleId = app_integer($data['ifrs16_schedule_id'] ?? 0);
        $ifrs16LinkedCount = ($ifrs16LeaseId > 0 || $ifrs16ScheduleId > 0) ? 1 : 0;
        $linkedDocumentCount = ($salesInvoiceId > 0 ? 1 : 0) + ($purchaseBillId > 0 ? 1 : 0) + ($expenseId > 0 ? 1 : 0) + $ifrs16LinkedCount;
        if ($linkedDocumentCount > 1) {
            throw new RuntimeException('Choose one linked document only: sales invoice, vendor bill, expense, or IFRS16 lease payment.');
        }
        if ($salesInvoiceId > 0) {
            $salesInvoice = app_sales_invoice_by_id($companyId, $salesInvoiceId);
            if (!$salesInvoice) {
                throw new RuntimeException('Selected sales invoice does not exist.');
            }
            $salesStatus = strtolower(app_normalize_whitespace((string) ($salesInvoice['status'] ?? '')));
            if (in_array($salesStatus, ['draft', 'cancelled'], true)) {
                throw new RuntimeException('Sales invoice must be posted before registering a collection.');
            }
            $direction = strtolower(app_normalize_whitespace((string) ($data['direction'] ?? '')));
            if ($direction !== 'in') {
                throw new RuntimeException('Sales invoice payments must be incoming payments.');
            }
        }
        if ($purchaseBillId > 0) {
            $purchaseBill = app_purchase_bill_by_id($companyId, $purchaseBillId);
            if (!$purchaseBill) {
                throw new RuntimeException('Selected vendor bill does not exist.');
            }
            $purchaseStatus = strtolower(app_normalize_whitespace((string) ($purchaseBill['status'] ?? '')));
            if (in_array($purchaseStatus, ['draft', 'cancelled'], true)) {
                throw new RuntimeException('Vendor bill must be posted before registering a payment.');
            }
            $direction = strtolower(app_normalize_whitespace((string) ($data['direction'] ?? '')));
            if ($direction !== 'out') {
                throw new RuntimeException('Vendor bill payments must be outgoing payments.');
            }
        }
        if ($expenseId > 0) {
            $expense = app_expense_by_id($companyId, $expenseId);
            if (!$expense) {
                throw new RuntimeException('Selected expense does not exist.');
            }
            $expenseStatus = strtolower(app_normalize_whitespace((string) ($expense['status'] ?? '')));
            if (in_array($expenseStatus, ['draft', 'cancelled'], true)) {
                throw new RuntimeException('Expense must be posted before registering a payment.');
            }
            $direction = strtolower(app_normalize_whitespace((string) ($data['direction'] ?? '')));
            if ($direction !== 'out') {
                throw new RuntimeException('Expense payments must be outgoing payments.');
            }
        }
        if ($ifrs16LinkedCount > 0) {
            $lease = null;
            $schedule = null;
            if ($ifrs16ScheduleId <= 0) {
                throw new RuntimeException('Select an IFRS16 payment line before registering a lease payment.');
            }
            if ($ifrs16ScheduleId > 0) {
                $schedule = app_ifrs16_schedule_by_id($companyId, $ifrs16ScheduleId);
                if (!$schedule) {
                    throw new RuntimeException('Selected IFRS16 schedule line does not exist.');
                }
                $submittedLeaseId = app_integer($data['__submitted_ifrs16_lease_id'] ?? $ifrs16LeaseId);
                $scheduleLeaseId = app_integer($schedule['lease_id'] ?? 0);
                if ($submittedLeaseId > 0 && $scheduleLeaseId > 0 && $submittedLeaseId !== $scheduleLeaseId) {
                    throw new RuntimeException('Selected IFRS16 payment line does not belong to the selected lease.');
                }
                $ifrs16LeaseId = app_integer($schedule['lease_id'] ?? 0);
            }
            if ($ifrs16LeaseId > 0) {
                $lease = app_ifrs16_lease_by_id($companyId, $ifrs16LeaseId);
            }
            if (!$lease) {
                throw new RuntimeException('Selected IFRS16 lease does not exist.');
            }
            $leaseStatus = strtolower(app_normalize_whitespace((string) ($lease['status'] ?? '')));
            if (in_array($leaseStatus, ['draft', 'cancelled'], true)) {
                throw new RuntimeException('IFRS16 lease must be active before registering a payment.');
            }
            $direction = strtolower(app_normalize_whitespace((string) ($data['direction'] ?? '')));
            if ($direction !== 'out') {
                throw new RuntimeException('IFRS16 lease payments must be outgoing payments.');
            }
            $remaining = $schedule ? app_ifrs16_schedule_remaining_amount($companyId, $schedule, $currentPaymentId > 0 ? $currentPaymentId : null) : 0.0;
            if ($remaining <= 0.01) {
                throw new RuntimeException('Selected IFRS16 payment line is already fully paid.');
            }
            if ($amount > $remaining + 0.01) {
                throw new RuntimeException('Payment amount cannot exceed IFRS16 payment line remaining amount (' . app_money_format($remaining) . ').');
            }
        }

        $contactCode = app_normalize_whitespace((string) ($data['contact_code'] ?? ''));
        if ($contactCode !== '' && !app_contact_exists($companyId, $contactCode)) {
            throw new RuntimeException('Selected contact does not exist.');
        }
        $bankName = app_normalize_whitespace((string) ($data['bank_account_name'] ?? ''));
        app_assert_payment_account_postable($companyId, $bankName, 'Cash/bank account', $paymentMethod);
        $offsetCode = app_normalize_whitespace((string) ($data['offset_account_code'] ?? ''));
        if ($offsetCode !== '') {
            $expectedOffsetType = $ifrs16LinkedCount > 0 ? 'liability' : null;
            app_assert_leaf_account($companyId, $offsetCode, $expectedOffsetType, 'Offset account');
        }
    }

    if ($slug === 'assets') {
        $expenseAccountCode = app_normalize_whitespace((string) ($data['depreciation_expense_account_code'] ?? ''));
        if ($expenseAccountCode !== '') {
            app_assert_leaf_account($companyId, $expenseAccountCode, 'expense', 'Depreciation expense account');
        }
        $accumulatedAccountCode = app_normalize_whitespace((string) ($data['depreciation_accumulated_account_code'] ?? ''));
        if ($accumulatedAccountCode !== '') {
            app_assert_leaf_account($companyId, $accumulatedAccountCode, 'asset', 'Accumulated depreciation account');
        }
    }

    if ($slug === 'prepaid_expenses') {
        $prepaidCode = app_normalize_whitespace((string) ($data['prepaid_account_code'] ?? ''));
        if ($prepaidCode === '') {
            app_posting_account_code($companyId, 'prepaid_expense', 'asset', ['1150', '1100']);
        } else {
            app_assert_leaf_account($companyId, $prepaidCode, 'asset', 'Prepaid expense account');
        }

        $expenseCode = app_normalize_whitespace((string) ($data['expense_account_code'] ?? ''));
        app_assert_leaf_account($companyId, $expenseCode, 'expense', 'Amortization expense account');

        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for prepaid expense amortization.');
        }

        $totalAmount = max(0, app_number($data['total_amount'] ?? 0));
        if ($totalAmount <= 0) {
            throw new RuntimeException('Prepaid expense amount must be greater than zero.');
        }

        $durationMonths = max(0, app_integer($data['duration_months'] ?? 0));
        if ($durationMonths <= 0) {
            throw new RuntimeException('Prepaid expense duration must be at least one month.');
        }

        $grandTotal = max(0, app_number($data['grand_total'] ?? $totalAmount));
        if ($grandTotal <= 0) {
            $taxTotal = max(0, app_number($data['tax_total'] ?? 0));
            $grandTotal = round($totalAmount + $taxTotal, 2);
        }

        $paidAmount = max(0, app_number($data['inline_paid_amount'] ?? 0));
        if ($paidAmount > $grandTotal) {
            throw new RuntimeException('Paid amount cannot exceed prepaid expense grand total.');
        }
        if ($paidAmount > 0) {
            $paymentMethod = app_normalize_whitespace((string) ($data['payment_method'] ?? ''));
            if (!in_array($paymentMethod, ['cash', 'bank', 'card', 'transfer'], true)) {
                throw new RuntimeException('Select a valid payment method for prepaid expense payment.');
            }
            $paymentAccountName = app_normalize_whitespace((string) ($data['payment_account_name'] ?? ''));
            app_assert_payment_account_postable($companyId, $paymentAccountName, 'Prepaid payment account', $paymentMethod);
        }
    }

    if ($slug === 'prepaid_amortization_runs') {
        $prepaidExpenseId = app_integer($data['prepaid_expense_id'] ?? 0);
        if ($prepaidExpenseId <= 0 || !app_prepaid_expense_by_id($companyId, $prepaidExpenseId)) {
            throw new RuntimeException('Select a valid prepaid expense.');
        }
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['prepaid_account_code'] ?? '')),
            'asset',
            'Prepaid expense account'
        );
        app_assert_leaf_account(
            $companyId,
            app_normalize_whitespace((string) ($data['expense_account_code'] ?? '')),
            'expense',
            'Amortization expense account'
        );
        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for prepaid amortization.');
        }
        if (max(0, app_number($data['amount'] ?? 0)) <= 0) {
            throw new RuntimeException('Amortization amount must be greater than zero.');
        }
    }

    if ($slug === 'ifrs16_leases') {
        $lessorCode = app_normalize_whitespace((string) ($data['lessor_code'] ?? ''));
        if ($lessorCode !== '' && !app_contact_exists($companyId, $lessorCode, 'vendor')) {
            throw new RuntimeException('Select a valid vendor/lessor contact for the IFRS16 lease.');
        }

        $costCenterCode = app_normalize_whitespace((string) ($data['cost_center_code'] ?? ''));
        if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Cost center is required for IFRS16 lease posting.');
        }

        $commencementDate = app_sanitize_date((string) ($data['commencement_date'] ?? ''), date('Y-m-d'));
        $endDate = app_sanitize_date((string) ($data['end_date'] ?? ''), $commencementDate);
        $firstPaymentDate = app_sanitize_date((string) ($data['first_payment_date'] ?? ''), $commencementDate);
        if ($endDate < $commencementDate) {
            throw new RuntimeException('Lease end date must be on or after commencement date.');
        }
        if ($firstPaymentDate <= $commencementDate || $firstPaymentDate > $endDate) {
            throw new RuntimeException('First payment date must be after commencement and on or before the lease end date.');
        }

        foreach ([
            'rou_asset_account_code' => ['asset', 'IFRS16 ROU asset account'],
            'lease_liability_account_code' => ['liability', 'IFRS16 lease liability account'],
            'interest_expense_account_code' => ['expense', 'IFRS16 interest expense account'],
            'depreciation_expense_account_code' => ['expense', 'IFRS16 depreciation expense account'],
            'accumulated_depreciation_account_code' => ['asset', 'IFRS16 accumulated depreciation account'],
            'restoration_provision_account_code' => ['liability', 'IFRS16 restoration provision account'],
            'lease_incentive_account_code' => ['asset', 'IFRS16 lease incentive account'],
        ] as $field => [$expectedType, $label]) {
            app_assert_leaf_account(
                $companyId,
                app_normalize_whitespace((string) ($data[$field] ?? '')),
                $expectedType,
                $label
            );
        }
    }

    if ($slug === 'ifrs16_lease_schedules') {
        $leaseId = app_integer($data['lease_id'] ?? 0);
        if ($leaseId <= 0 || !app_ifrs16_lease_by_id($companyId, $leaseId)) {
            throw new RuntimeException('Select a valid IFRS16 lease for the schedule line.');
        }
        $amount = app_number($data['payment_amount'] ?? 0) + app_number($data['interest_expense'] ?? 0) + app_number($data['depreciation_expense'] ?? 0);
        if ($amount <= 0) {
            throw new RuntimeException('IFRS16 schedule line must have payment, interest, or depreciation amount.');
        }
    }

    if ($slug === 'depreciation_runs') {
        $expenseAccountCode = app_normalize_whitespace((string) ($data['expense_account_code'] ?? ''));
        if ($expenseAccountCode !== '') {
            app_assert_leaf_account($companyId, $expenseAccountCode, 'expense', 'Depreciation expense account');
        }
        $accumulatedAccountCode = app_normalize_whitespace((string) ($data['accumulated_account_code'] ?? ''));
        if ($accumulatedAccountCode !== '') {
            app_assert_leaf_account($companyId, $accumulatedAccountCode, 'asset', 'Accumulated depreciation account');
        }
    }
}

function app_module_requires_auto_posting(string $slug): bool
{
    return in_array($slug, ['sales_invoices', 'sales_returns', 'student_invoices', 'student_receipts', 'student_refunds', 'purchase_bills', 'purchase_returns', 'expenses', 'expense_returns', 'payments', 'prepaid_expenses', 'prepaid_amortization_runs', 'depreciation_runs', 'ifrs16_lease_schedules', 'pos_orders', 'pos_sessions', 'pos_cash_movements', 'excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'], true);
}

function app_module_workflow_definition(string $slug): array
{
    return match ($slug) {
        'sales_invoices' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'issued' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'issued' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'issued', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['issued', 'cancelled']],
            ],
        ],
        'sales_returns' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'issued' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'issued' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'issued', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['issued', 'cancelled']],
            ],
        ],
        'student_invoices' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'issued' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'issued' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'issued', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['issued', 'cancelled']],
            ],
        ],
        'purchase_bills' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'approved' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'approved' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'approved', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['approved', 'cancelled']],
            ],
        ],
        'purchase_returns' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'approved' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'approved' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'approved', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['approved', 'cancelled']],
            ],
        ],
        'expenses' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'approved' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'approved' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'approved', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['approved', 'cancelled']],
            ],
        ],
        'expense_returns' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'approved' => 'Posted',
                'partially_paid' => 'Partially Paid',
                'paid' => 'Paid',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'approved' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'approved', 'partially_paid']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['approved', 'cancelled']],
            ],
        ],
        'journal_entries' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'reversed' => 'Reversed',
            ],
            'actions' => [
                'posted' => ['label' => 'Post', 'primary' => true, 'from' => ['draft']],
                'reversed' => ['label' => 'Reverse', 'danger' => true, 'from' => ['posted']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['posted', 'reversed']],
            ],
        ],
        'prepaid_expenses' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'active' => 'Active',
                'closed' => 'Closed',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'active' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'closed' => ['label' => 'Close', 'from' => ['active']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'active']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['active', 'cancelled']],
            ],
        ],
        'prepaid_amortization_runs' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
            ],
        ],
        'depreciation_runs' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
            ],
        ],
        'ifrs16_leases' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'active' => 'Active',
                'closed' => 'Closed',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'active' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'closed' => ['label' => 'Close', 'from' => ['active']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'active']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['active', 'cancelled']],
            ],
        ],
        'ifrs16_lease_schedules' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'posted' => ['label' => 'Post', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['posted', 'cancelled']],
            ],
        ],
        'excellence_operations' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'confirmed' => 'Confirmed',
                'collected' => 'Collected',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'confirmed' => ['label' => 'Confirm', 'primary' => true, 'from' => ['draft']],
                'collected' => ['label' => 'Mark Collected', 'primary' => true, 'from' => ['confirmed']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft', 'confirmed']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['confirmed', 'cancelled']],
            ],
        ],
        'excellence_hospital_expenses' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'posted' => ['label' => 'Post Payment', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['posted', 'cancelled']],
            ],
        ],
        'excellence_admin_expenses' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'posted' => ['label' => 'Post Expense', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['posted', 'cancelled']],
            ],
        ],
        'excellence_salaries' => [
            'field' => 'status',
            'default' => 'draft',
            'states' => [
                'draft' => 'Draft',
                'posted' => 'Posted',
                'cancelled' => 'Cancelled',
            ],
            'actions' => [
                'posted' => ['label' => 'Post Salary', 'primary' => true, 'from' => ['draft']],
                'cancelled' => ['label' => 'Cancel', 'danger' => true, 'from' => ['draft']],
                'draft' => ['label' => 'Reset to Draft', 'from' => ['posted', 'cancelled']],
            ],
        ],
        default => [],
    };
}

function app_module_workflow_transition(string $slug, int $id, ?int $companyId, string $targetStatus): void
{
    $workflow = app_module_workflow_definition($slug);
    if (!$workflow) {
        throw new RuntimeException('This module does not support workflow actions.');
    }

    app_enforce_current_role_can($slug, 'post', 'You do not have permission to post or change workflow status in this module.');

    $definition = app_module_assert_access($slug);
    $table = (string) ($definition['table'] ?? '');
    $field = (string) ($workflow['field'] ?? 'status');
    $targetStatus = app_normalize_whitespace($targetStatus);
    $states = (array) ($workflow['states'] ?? []);
    if ($id <= 0 || $targetStatus === '' || !array_key_exists($targetStatus, $states)) {
        throw new RuntimeException('Invalid workflow action.');
    }
    if (!app_identifier($table) || !app_identifier($field)) {
        throw new RuntimeException('Invalid workflow configuration.');
    }

    $record = app_module_find($slug, $id, $companyId);
    if (!$record) {
        throw new RuntimeException('Record was not found.');
    }

    $currentStatus = app_normalize_whitespace((string) ($record[$field] ?? ($workflow['default'] ?? 'draft')));
    $actions = (array) ($workflow['actions'] ?? []);
    $action = (array) ($actions[$targetStatus] ?? []);
    $allowedFrom = (array) ($action['from'] ?? []);
    if ($allowedFrom && !in_array($currentStatus, $allowedFrom, true)) {
        throw new RuntimeException('This workflow action is not available from the current status.');
    }

    if (in_array($slug, ['sales_invoices', 'sales_returns', 'student_invoices', 'purchase_bills', 'purchase_returns', 'expenses', 'expense_returns', 'prepaid_expenses', 'ifrs16_leases', 'ifrs16_lease_schedules', 'excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'], true) && !in_array($targetStatus, ['draft', 'cancelled'], true)) {
        app_validate_transaction_master_links($slug, $record, $companyId);
    }

    if ($slug === 'journal_entries' && $targetStatus === 'posted') {
        if ($companyId === null || $companyId <= 0) {
            throw new RuntimeException('Company context is required.');
        }
        $lineSummary = app_validate_journal_entry_lines(
            (int) $companyId,
            app_journal_entry_lines((int) $companyId, (int) $id)
        );
        app_pdo()->prepare(
            'UPDATE journal_entries
             SET total_debit = :total_debit,
                 total_credit = :total_credit
             WHERE id = :id
               AND company_id = :company_id'
        )->execute([
            'total_debit' => (float) ($lineSummary['total_debit'] ?? 0),
            'total_credit' => (float) ($lineSummary['total_credit'] ?? 0),
            'id' => $id,
            'company_id' => (int) $companyId,
        ]);
    }

    $pdo = app_pdo();
    $params = [
        'status' => $targetStatus,
        'id' => $id,
    ];
    $sql = 'UPDATE `' . $table . '` SET `' . $field . '` = :status WHERE id = :id';
    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            throw new RuntimeException('Company context is required.');
        }
        $sql .= ' AND company_id = :company_id';
        $params['company_id'] = $companyId;
    }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);

    if (app_module_requires_auto_posting($slug) && $companyId !== null && $companyId > 0) {
        $updated = app_module_find($slug, $id, $companyId);
        if ($updated) {
            app_sync_auto_posting_for_record($slug, $updated, $companyId);
        }
    }
}

function app_module_record_should_post(string $slug, array $record): bool
{
    if (in_array($slug, ['sales_invoices', 'sales_returns', 'student_invoices', 'purchase_bills', 'purchase_returns', 'expenses', 'expense_returns'], true)) {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        return !in_array($status, ['draft', 'cancelled'], true);
    }

    if (in_array($slug, ['student_receipts', 'student_refunds'], true)) {
        return app_number($record['amount'] ?? 0) > 0;
    }

    if ($slug === 'payments') {
        return app_number($record['amount'] ?? 0) > 0;
    }

    if ($slug === 'pos_orders') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'paid')));
        return $status !== 'cancelled' && abs(app_number($record['grand_total'] ?? 0)) > 0;
    }

    if ($slug === 'pos_cash_movements') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'posted')));
        return $status === 'posted' && app_number($record['amount'] ?? 0) > 0;
    }

    if ($slug === 'pos_sessions') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'open')));
        return $status === 'closed' && abs(app_number($record['cash_difference'] ?? 0)) >= 0.01;
    }

    if ($slug === 'prepaid_expenses') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        return in_array($status, ['active', 'closed'], true) && app_number($record['total_amount'] ?? 0) > 0;
    }

    if ($slug === 'prepaid_amortization_runs') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        return $status !== 'cancelled' && app_number($record['amount'] ?? 0) > 0;
    }

    if ($slug === 'depreciation_runs') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        return $status !== 'cancelled' && app_number($record['total_amount'] ?? 0) > 0;
    }

    if ($slug === 'ifrs16_lease_schedules') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        $amount = app_number($record['payment_amount'] ?? 0) + app_number($record['interest_expense'] ?? 0) + app_number($record['depreciation_expense'] ?? 0);
        return $status !== 'cancelled' && $amount > 0;
    }

    if ($slug === 'excellence_operations') {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        $amount = app_number($record['revenue_amount'] ?? 0) + app_number($record['hospital_expense_amount'] ?? 0);
        return !in_array($status, ['draft', 'cancelled'], true) && $amount > 0;
    }

    if (in_array($slug, ['excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'], true)) {
        $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        return $status === 'posted' && app_number($record['amount'] ?? 0) > 0;
    }

    return false;
}

function app_accounting_line(string $accountCode, float $debit, float $credit, string $description, ?string $costCenterCode = null): array
{
    return [
        'account_code' => app_normalize_whitespace($accountCode),
        'cost_center_code' => app_normalize_whitespace((string) ($costCenterCode ?? '')),
        'debit' => max(0, app_number($debit)),
        'credit' => max(0, app_number($credit)),
        'description' => app_normalize_whitespace($description),
    ];
}

function app_document_line_journal_lines(int $companyId, string $slug, array $record, string $documentLabel): array
{
    $config = app_document_line_config($slug);
    if ($config === null || $companyId <= 0) {
        return [];
    }

    $documentId = (int) ($record['id'] ?? 0);
    $documentLines = app_fetch_document_lines($slug, $companyId, $documentId, $record);
    if (!$documentLines) {
        return [];
    }

    $isRevenueDocument = (string) $config['account_type'] === 'revenue';
    $fallbacks = $isRevenueDocument ? ['4100', '4110', '4120'] : ['5210', '5100', '5510'];
    $defaultAccount = app_normalize_whitespace((string) ($record[(string) $config['account_field']] ?? ''));
    $defaultCostCenter = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $result = [];

    foreach ($documentLines as $line) {
        $base = round(max(0, app_number($line['subtotal'] ?? 0)), 2);
        $tax = round(max(0, app_number($line['tax_total'] ?? 0)), 2);
        if ($base <= 0 && $tax <= 0) {
            continue;
        }

        $lineAccount = app_normalize_whitespace((string) ($line['account_code'] ?? ''));
        if ($lineAccount === '') {
            $lineAccount = $defaultAccount;
        }
        $lineAccount = app_resolve_account_code($companyId, $lineAccount, (string) $config['account_type'], $fallbacks);
        $costCenter = app_normalize_whitespace((string) ($line['cost_center_code'] ?? ''));
        if ($costCenter === '') {
            $costCenter = $defaultCostCenter;
        }
        $lineDescription = app_normalize_whitespace((string) ($line['description'] ?? ''));
        $memo = $documentLabel . ($lineDescription !== '' ? ' - ' . $lineDescription : '');

        if ($base > 0) {
            $result[] = $isRevenueDocument
                ? app_accounting_line($lineAccount, 0, $base, $memo, $costCenter)
                : app_accounting_line($lineAccount, $base, 0, $memo, $costCenter);
        }
        if ($tax > 0) {
            $taxCode = app_tax_posting_account_code($companyId, (string) ($line['tax_name'] ?? ''), (string) $config['tax_scope']);
            if ($taxCode === null) {
                $taxCode = $isRevenueDocument
                    ? app_resolve_account_code($companyId, '', 'liability', ['2120'])
                    : app_resolve_account_code($companyId, '', 'asset', ['1140']);
            }
            $result[] = $isRevenueDocument
                ? app_accounting_line($taxCode, 0, $tax, $documentLabel . ' output VAT')
                : app_accounting_line($taxCode, $tax, 0, $documentLabel . ' input VAT');
        }
    }

    return $result;
}

function app_build_sales_invoice_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    $taxTotal = max(0, app_number($record['tax_total'] ?? 0));
    if ($taxTotal > $grandTotal) {
        $taxTotal = $grandTotal;
    }
    $revenueValue = max(0, $grandTotal - $taxTotal);
    if ($grandTotal <= 0) {
        return [];
    }

    $paidAmount = max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0)));
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }
    $receivableAmount = max(0, $grandTotal - $paidAmount);

    $receivableCode = app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $revenueCode = app_resolve_account_code(
        $companyId,
        (string) ($record['revenue_account_code'] ?? ''),
        'revenue',
        ['4100', '4110', '4120']
    );
    $taxOutputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'sales')
        ?? app_resolve_account_code($companyId, '', 'liability', ['2120']);
    $paymentAccountCode = '';
    if ($paidAmount > 0) {
        $paymentAccountCode = app_resolve_account_code(
            $companyId,
            app_resolve_payment_account_gl_account_code(
                $companyId,
                (string) ($record['payment_account_name'] ?? ''),
                (string) ($record['payment_method'] ?? '')
            ) ?? '',
            'asset',
            ['1112', '1111', '1100']
        );
    }

    $invoiceNumber = (string) ($record['invoice_number'] ?? '');
    $linePostings = app_document_line_journal_lines($companyId, 'sales_invoices', $record, 'Invoice ' . $invoiceNumber);
    $lines = [];
    if ($receivableAmount > 0) {
        $lines[] = app_accounting_line($receivableCode, $receivableAmount, 0, 'Invoice ' . $invoiceNumber . ' receivable');
    }
    if ($paidAmount > 0) {
        $lines[] = app_accounting_line($paymentAccountCode, $paidAmount, 0, 'Invoice ' . $invoiceNumber . ' collection');
    }
    if ($linePostings) {
        return array_merge($lines, $linePostings);
    }
    if ($revenueValue > 0) {
        $lines[] = app_accounting_line($revenueCode, 0, $revenueValue, 'Invoice ' . $invoiceNumber . ' revenue', $costCenterCode);
    }
    if ($taxTotal > 0) {
        $lines[] = app_accounting_line($taxOutputCode, 0, $taxTotal, 'Invoice ' . $invoiceNumber . ' output VAT');
    }
    return $lines;
}

function app_build_student_invoice_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    $taxTotal = max(0, app_number($record['tax_total'] ?? 0));
    if ($taxTotal > $grandTotal) {
        $taxTotal = $grandTotal;
    }
    $revenueValue = max(0, $grandTotal - $taxTotal);
    if ($grandTotal <= 0) {
        return [];
    }

    $paidAmount = max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0)));
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }
    $receivableAmount = max(0, $grandTotal - $paidAmount);

    $receivableCode = app_student_setting_account_code($companyId, 'student_receivable', 'asset', ['1121', '1120']);
    $revenueCode = app_resolve_account_code(
        $companyId,
        (string) ($record['revenue_account_code'] ?? ''),
        'revenue',
        ['4100', '4110', '4120']
    );
    $taxOutputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'sales')
        ?? app_resolve_account_code($companyId, '', 'liability', ['2120']);
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    if ($costCenterCode === '') {
        $settings = app_student_account_settings($companyId);
        $costCenterCode = app_normalize_whitespace((string) ($settings['default_cost_center_code'] ?? ''));
    }
    $paymentAccountCode = '';
    if ($paidAmount > 0) {
        $paymentAccountCode = app_resolve_account_code(
            $companyId,
            app_resolve_payment_account_gl_account_code(
                $companyId,
                (string) ($record['payment_account_name'] ?? ''),
                (string) ($record['payment_method'] ?? '')
            ) ?? '',
            'asset',
            ['1112', '1111', '1100']
        );
    }

    $invoiceNumber = (string) ($record['student_invoice_number'] ?? '');
    $studentNumber = (string) ($record['student_number'] ?? '');
    $label = trim('Student invoice ' . $invoiceNumber . ($studentNumber !== '' ? ' / ' . $studentNumber : ''));
    $linePostings = app_document_line_journal_lines($companyId, 'student_invoices', $record, $label);

    $lines = [];
    if ($receivableAmount > 0) {
        $lines[] = app_accounting_line($receivableCode, $receivableAmount, 0, $label . ' receivable');
    }
    if ($paidAmount > 0) {
        $lines[] = app_accounting_line($paymentAccountCode, $paidAmount, 0, $label . ' collection');
    }
    if ($linePostings) {
        return array_merge($lines, $linePostings);
    }
    if ($revenueValue > 0) {
        $lines[] = app_accounting_line($revenueCode, 0, $revenueValue, $label . ' revenue', $costCenterCode);
    }
    if ($taxTotal > 0) {
        $lines[] = app_accounting_line($taxOutputCode, 0, $taxTotal, $label . ' output VAT');
    }
    return $lines;
}

function app_build_student_receipt_journal_lines(int $companyId, array $record): array
{
    $amount = max(0, app_number($record['amount'] ?? 0));
    if ($amount <= 0) {
        return [];
    }

    $paymentCode = app_resolve_account_code(
        $companyId,
        app_resolve_payment_account_gl_account_code(
            $companyId,
            (string) ($record['payment_account_name'] ?? ''),
            (string) ($record['payment_method'] ?? '')
        ) ?? '',
        'asset',
        ['1112', '1111', '1100']
    );
    $invoiceId = app_integer($record['student_invoice_id'] ?? 0);
    $creditCode = $invoiceId > 0
        ? app_student_setting_account_code($companyId, 'student_receivable', 'asset', ['1121', '1120'])
        : app_student_setting_account_code($companyId, 'student_advance', 'liability', ['2140', '2100']);
    $receiptNumber = (string) ($record['receipt_number'] ?? '');
    return [
        app_accounting_line($paymentCode, $amount, 0, 'Student receipt ' . $receiptNumber . ' collection'),
        app_accounting_line($creditCode, 0, $amount, 'Student receipt ' . $receiptNumber . ($invoiceId > 0 ? ' against invoice' : ' advance')),
    ];
}

function app_build_student_refund_journal_lines(int $companyId, array $record): array
{
    $amount = max(0, app_number($record['amount'] ?? 0));
    if ($amount <= 0) {
        return [];
    }

    $paymentCode = app_resolve_account_code(
        $companyId,
        app_resolve_payment_account_gl_account_code(
            $companyId,
            (string) ($record['payment_account_name'] ?? ''),
            (string) ($record['payment_method'] ?? '')
        ) ?? '',
        'asset',
        ['1112', '1111', '1100']
    );
    $offsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
    if ($offsetCode !== '') {
        app_assert_leaf_account($companyId, $offsetCode, null, 'Student voucher offset account');
    } else {
        $offsetCode = app_student_setting_account_code($companyId, 'student_advance', 'liability', ['2140', '2100']);
    }
    $voucherNumber = (string) ($record['voucher_number'] ?? '');
    return [
        app_accounting_line($offsetCode, $amount, 0, 'Student payment voucher ' . $voucherNumber),
        app_accounting_line($paymentCode, 0, $amount, 'Student payment voucher ' . $voucherNumber . ' paid'),
    ];
}

function app_build_purchase_bill_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    $taxTotal = max(0, app_number($record['tax_total'] ?? 0));
    if ($taxTotal > $grandTotal) {
        $taxTotal = $grandTotal;
    }
    $expenseValue = max(0, $grandTotal - $taxTotal);
    if ($grandTotal <= 0) {
        return [];
    }

    $withholdingTaxTotal = max(0, app_number($record['withholding_tax_total'] ?? 0));
    if ($withholdingTaxTotal > $grandTotal) {
        $withholdingTaxTotal = $grandTotal;
    }
    $paymentBasisTotal = max(0, $grandTotal - $withholdingTaxTotal);
    $paidAmount = max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0)));
    if ($paidAmount > $paymentBasisTotal) {
        $paidAmount = $paymentBasisTotal;
    }
    $payableAmount = max(0, $paymentBasisTotal - $paidAmount);

    $payableCode = '';
    if ($payableAmount > 0) {
        $payableCode = app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100']);
    }
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $expenseCode = app_resolve_account_code(
        $companyId,
        (string) ($record['expense_account_code'] ?? ''),
        'expense',
        ['5210', '5100', '5510']
    );
    $taxInputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'purchase')
        ?? app_resolve_account_code($companyId, '', 'asset', ['1140']);
    $withholdingTaxCode = '';
    if ($withholdingTaxTotal > 0) {
        $withholdingTaxCode = app_tax_posting_account_code($companyId, (string) ($record['withholding_tax_name'] ?? ''), 'withholding')
            ?? app_resolve_account_code($companyId, '', 'liability', ['2160', '2130', '2111', '2100']);
    }
    $paymentAccountCode = '';
    if ($paidAmount > 0) {
        $paymentAccountCode = app_resolve_account_code(
            $companyId,
            app_resolve_payment_account_gl_account_code(
                $companyId,
                (string) ($record['payment_account_name'] ?? ''),
                (string) ($record['payment_method'] ?? '')
            ) ?? '',
            'asset',
            ['1112', '1111', '1100']
        );
    }

    $billNumber = (string) ($record['bill_number'] ?? '');
    $linePostings = app_document_line_journal_lines($companyId, 'purchase_bills', $record, 'Bill ' . $billNumber);
    $lines = [];
    if ($linePostings) {
        $lines = array_merge($lines, $linePostings);
    } elseif ($expenseValue > 0) {
        $lines[] = app_accounting_line($expenseCode, $expenseValue, 0, 'Bill ' . $billNumber . ' expense', $costCenterCode);
    }
    if (!$linePostings && $taxTotal > 0) {
        $lines[] = app_accounting_line($taxInputCode, $taxTotal, 0, 'Bill ' . $billNumber . ' input VAT');
    }
    if ($withholdingTaxTotal > 0) {
        $lines[] = app_accounting_line($withholdingTaxCode, 0, $withholdingTaxTotal, 'Bill ' . $billNumber . ' withholding tax');
    }
    if ($payableAmount > 0) {
        $lines[] = app_accounting_line($payableCode, 0, $payableAmount, 'Bill ' . $billNumber . ' payable');
    }
    if ($paidAmount > 0) {
        $lines[] = app_accounting_line($paymentAccountCode, 0, $paidAmount, 'Bill ' . $billNumber . ' payment');
    }
    return $lines;
}

function app_build_expense_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    $taxTotal = max(0, app_number($record['tax_total'] ?? 0));
    if ($taxTotal > $grandTotal) {
        $taxTotal = $grandTotal;
    }
    $expenseValue = max(0, $grandTotal - $taxTotal);
    if ($grandTotal <= 0) {
        return [];
    }

    $paidAmount = max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0)));
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }
    $payableAmount = max(0, $grandTotal - $paidAmount);

    $expenseOffsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
    if ($expenseOffsetCode === '') {
        $expenseOffsetCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
    }
    $payableCode = app_resolve_account_code($companyId, $expenseOffsetCode, 'liability', ['2130', '2111', '2100']);
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $expenseCode = app_resolve_account_code(
        $companyId,
        (string) ($record['expense_account_code'] ?? ''),
        'expense',
        ['5210', '5100', '5510']
    );
    $taxInputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'purchase')
        ?? app_resolve_account_code($companyId, '', 'asset', ['1140']);
    $paymentAccountCode = '';
    if ($paidAmount > 0) {
        $paymentAccountCode = app_resolve_account_code(
            $companyId,
            app_resolve_payment_account_gl_account_code(
                $companyId,
                (string) ($record['payment_account_name'] ?? ''),
                (string) ($record['payment_method'] ?? '')
            ) ?? '',
            'asset',
            ['1112', '1111', '1100']
        );
    }

    $expenseNumber = (string) ($record['expense_number'] ?? '');
    $linePostings = app_document_line_journal_lines($companyId, 'expenses', $record, 'Expense ' . $expenseNumber);
    $lines = [];
    if ($linePostings) {
        $lines = array_merge($lines, $linePostings);
    } elseif ($expenseValue > 0) {
        $lines[] = app_accounting_line($expenseCode, $expenseValue, 0, 'Expense ' . $expenseNumber . ' cost', $costCenterCode);
    }
    if (!$linePostings && $taxTotal > 0) {
        $lines[] = app_accounting_line($taxInputCode, $taxTotal, 0, 'Expense ' . $expenseNumber . ' input VAT');
    }
    if ($payableAmount > 0) {
        $lines[] = app_accounting_line($payableCode, 0, $payableAmount, 'Expense ' . $expenseNumber . ' payable');
    }
    if ($paidAmount > 0) {
        $lines[] = app_accounting_line($paymentAccountCode, 0, $paidAmount, 'Expense ' . $expenseNumber . ' payment');
    }
    return $lines;
}

function app_reverse_accounting_lines(array $lines, string $memoSuffix = ' reversal'): array
{
    $reversed = [];
    foreach ($lines as $line) {
        $description = app_normalize_whitespace((string) ($line['description'] ?? ''));
        $reversed[] = app_accounting_line(
            (string) ($line['account_code'] ?? ''),
            max(0, app_number($line['credit'] ?? 0)),
            max(0, app_number($line['debit'] ?? 0)),
            trim($description . $memoSuffix),
            (string) ($line['cost_center_code'] ?? '')
        );
    }
    return $reversed;
}

function app_build_sales_return_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    if ($grandTotal <= 0) {
        return [];
    }
    $refundAmount = min($grandTotal, max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0))));
    $receivableAmount = max(0, $grandTotal - $refundAmount);
    $returnNumber = (string) ($record['return_number'] ?? '');

    $sourceLines = app_document_line_journal_lines($companyId, 'sales_returns', $record, 'Sales return ' . $returnNumber);
    if (!$sourceLines) {
        $taxTotal = min($grandTotal, max(0, app_number($record['tax_total'] ?? 0)));
        $revenueValue = max(0, $grandTotal - $taxTotal);
        $revenueCode = app_resolve_account_code($companyId, (string) ($record['revenue_account_code'] ?? ''), 'revenue', ['4100', '4110', '4120']);
        $taxOutputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'sales')
            ?? app_resolve_account_code($companyId, '', 'liability', ['2120']);
        if ($revenueValue > 0) {
            $sourceLines[] = app_accounting_line($revenueCode, 0, $revenueValue, 'Sales return ' . $returnNumber . ' revenue', (string) ($record['cost_center_code'] ?? ''));
        }
        if ($taxTotal > 0) {
            $sourceLines[] = app_accounting_line($taxOutputCode, 0, $taxTotal, 'Sales return ' . $returnNumber . ' output VAT');
        }
    }

    $lines = app_reverse_accounting_lines($sourceLines);
    if ($refundAmount > 0) {
        $paymentCode = app_resolve_account_code($companyId, app_resolve_payment_account_gl_account_code($companyId, (string) ($record['payment_account_name'] ?? ''), (string) ($record['payment_method'] ?? '')) ?? '', 'asset', ['1112', '1111', '1100']);
        $lines[] = app_accounting_line($paymentCode, 0, $refundAmount, 'Sales return ' . $returnNumber . ' refund');
    }
    if ($receivableAmount > 0) {
        $receivableCode = app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
        $lines[] = app_accounting_line($receivableCode, 0, $receivableAmount, 'Sales return ' . $returnNumber . ' receivable reduction');
    }
    return $lines;
}

function app_build_purchase_return_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    if ($grandTotal <= 0) {
        return [];
    }
    $refundAmount = min($grandTotal, max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0))));
    $payableReduction = max(0, $grandTotal - $refundAmount);
    $returnNumber = (string) ($record['return_number'] ?? '');

    $sourceLines = app_document_line_journal_lines($companyId, 'purchase_returns', $record, 'Purchase return ' . $returnNumber);
    if (!$sourceLines) {
        $taxTotal = min($grandTotal, max(0, app_number($record['tax_total'] ?? 0)));
        $expenseValue = max(0, $grandTotal - $taxTotal);
        $expenseCode = app_resolve_account_code($companyId, (string) ($record['expense_account_code'] ?? ''), 'expense', ['5210', '5100', '5510']);
        $taxInputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'purchase')
            ?? app_resolve_account_code($companyId, '', 'asset', ['1140']);
        if ($expenseValue > 0) {
            $sourceLines[] = app_accounting_line($expenseCode, $expenseValue, 0, 'Purchase return ' . $returnNumber . ' expense', (string) ($record['cost_center_code'] ?? ''));
        }
        if ($taxTotal > 0) {
            $sourceLines[] = app_accounting_line($taxInputCode, $taxTotal, 0, 'Purchase return ' . $returnNumber . ' input VAT');
        }
    }

    $lines = [];
    if ($refundAmount > 0) {
        $paymentCode = app_resolve_account_code($companyId, app_resolve_payment_account_gl_account_code($companyId, (string) ($record['payment_account_name'] ?? ''), (string) ($record['payment_method'] ?? '')) ?? '', 'asset', ['1112', '1111', '1100']);
        $lines[] = app_accounting_line($paymentCode, $refundAmount, 0, 'Purchase return ' . $returnNumber . ' refund received');
    }
    if ($payableReduction > 0) {
        $payableCode = app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100']);
        $lines[] = app_accounting_line($payableCode, $payableReduction, 0, 'Purchase return ' . $returnNumber . ' payable reduction');
    }
    return array_merge($lines, app_reverse_accounting_lines($sourceLines));
}

function app_build_expense_return_journal_lines(int $companyId, array $record): array
{
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    if ($grandTotal <= 0) {
        return [];
    }
    $refundAmount = min($grandTotal, max(0, app_number($record['inline_paid_amount'] ?? ($record['paid_amount'] ?? 0))));
    $payableReduction = max(0, $grandTotal - $refundAmount);
    $returnNumber = (string) ($record['return_number'] ?? '');
    $sourceLines = app_document_line_journal_lines($companyId, 'expense_returns', $record, 'Expense return ' . $returnNumber);
    if (!$sourceLines) {
        $taxTotal = min($grandTotal, max(0, app_number($record['tax_total'] ?? 0)));
        $expenseValue = max(0, $grandTotal - $taxTotal);
        $expenseCode = app_resolve_account_code($companyId, (string) ($record['expense_account_code'] ?? ''), 'expense', ['5210', '5100', '5510']);
        $taxInputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'purchase')
            ?? app_resolve_account_code($companyId, '', 'asset', ['1140']);
        if ($expenseValue > 0) {
            $sourceLines[] = app_accounting_line($expenseCode, $expenseValue, 0, 'Expense return ' . $returnNumber . ' cost', (string) ($record['cost_center_code'] ?? ''));
        }
        if ($taxTotal > 0) {
            $sourceLines[] = app_accounting_line($taxInputCode, $taxTotal, 0, 'Expense return ' . $returnNumber . ' input VAT');
        }
    }
    $lines = [];
    if ($refundAmount > 0) {
        $paymentCode = app_resolve_account_code($companyId, app_resolve_payment_account_gl_account_code($companyId, (string) ($record['payment_account_name'] ?? ''), (string) ($record['payment_method'] ?? '')) ?? '', 'asset', ['1112', '1111', '1100']);
        $lines[] = app_accounting_line($paymentCode, $refundAmount, 0, 'Expense return ' . $returnNumber . ' refund received');
    }
    if ($payableReduction > 0) {
        $offsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
        if ($offsetCode === '') {
            $offsetCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
        }
        $lines[] = app_accounting_line(app_resolve_account_code($companyId, $offsetCode, 'liability', ['2130', '2111', '2100']), $payableReduction, 0, 'Expense return ' . $returnNumber . ' payable reduction');
    }
    return array_merge($lines, app_reverse_accounting_lines($sourceLines));
}

function app_build_excellence_operation_journal_lines(int $companyId, array $record): array
{
    $revenueAmount = round(max(0, app_number($record['revenue_amount'] ?? 0)), 2);
    $hospitalExpenseAmount = round(max(0, app_number($record['hospital_expense_amount'] ?? 0)), 2);
    if ($revenueAmount <= 0 && $hospitalExpenseAmount <= 0) {
        return [];
    }

    $operationNumber = app_normalize_whitespace((string) ($record['operation_number'] ?? ''));
    $hospitalCode = app_normalize_whitespace((string) ($record['hospital_code'] ?? ''));
    $patientName = app_normalize_whitespace((string) ($record['patient_name'] ?? ''));
    $memoBase = trim('EXcellence operation ' . $operationNumber . ($patientName !== '' ? ' / ' . $patientName : '') . ($hospitalCode !== '' ? ' / ' . $hospitalCode : ''));
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $lines = [];

    if ($revenueAmount > 0) {
        $receivableCode = app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
        $revenueCode = app_resolve_account_code(
            $companyId,
            (string) ($record['revenue_account_code'] ?? ''),
            'revenue',
            ['4200', '4100', '4110']
        );
        $lines[] = app_accounting_line($receivableCode, $revenueAmount, 0, $memoBase . ' receivable');
        $lines[] = app_accounting_line($revenueCode, 0, $revenueAmount, $memoBase . ' device rental revenue', $costCenterCode);
    }

    if ($hospitalExpenseAmount > 0) {
        $expenseCode = app_resolve_account_code(
            $companyId,
            (string) ($record['hospital_expense_account_code'] ?? ''),
            'expense',
            ['5530', '5210', '5510']
        );
        $accruedCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
        $lines[] = app_accounting_line($expenseCode, $hospitalExpenseAmount, 0, $memoBase . ' hospital operation cost', $costCenterCode);
        $lines[] = app_accounting_line($accruedCode, 0, $hospitalExpenseAmount, $memoBase . ' accrued hospital payable');
    }

    return $lines;
}

function app_build_excellence_hospital_expense_journal_lines(int $companyId, array $record): array
{
    $amount = round(max(0, app_number($record['amount'] ?? 0)), 2);
    if ($amount <= 0) {
        return [];
    }

    $paymentCode = app_resolve_account_code(
        $companyId,
        app_resolve_payment_account_gl_account_code(
            $companyId,
            (string) ($record['payment_account_name'] ?? ''),
            (string) ($record['payment_method'] ?? '')
        ) ?? '',
        'asset',
        ['1112', '1111', '1100']
    );
    $expenseNumber = app_normalize_whitespace((string) ($record['expense_number'] ?? ''));
    $operationNumber = app_normalize_whitespace((string) ($record['operation_number'] ?? ''));
    $memo = trim('EXcellence hospital payment ' . $expenseNumber . ($operationNumber !== '' ? ' for ' . $operationNumber : ''));
    $settlementMode = app_normalize_whitespace((string) ($record['settlement_mode'] ?? 'accrued_operation'));

    if ($settlementMode === 'direct_expense') {
        $expenseCode = app_resolve_account_code(
            $companyId,
            (string) ($record['expense_account_code'] ?? ''),
            'expense',
            ['5530', '5210', '5510']
        );
        return [
            app_accounting_line($expenseCode, $amount, 0, $memo . ' direct cost', app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''))),
            app_accounting_line($paymentCode, 0, $amount, $memo . ' paid'),
        ];
    }

    $offsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
    if ($offsetCode === '') {
        $offsetCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
    } else {
        $offsetCode = app_resolve_account_code($companyId, $offsetCode, 'liability', ['2130', '2111', '2100']);
    }

    return [
        app_accounting_line($offsetCode, $amount, 0, $memo . ' accrued settlement'),
        app_accounting_line($paymentCode, 0, $amount, $memo . ' paid'),
    ];
}

function app_build_excellence_paid_expense_journal_lines(int $companyId, array $record, string $numberField, string $dateLabel, string $expenseAccountField): array
{
    $amount = round(max(0, app_number($record['amount'] ?? 0)), 2);
    if ($amount <= 0) {
        return [];
    }

    $expenseCode = app_resolve_account_code(
        $companyId,
        (string) ($record[$expenseAccountField] ?? ''),
        'expense',
        ['5510', '5530', '5100']
    );
    $paymentCode = app_resolve_account_code(
        $companyId,
        app_resolve_payment_account_gl_account_code(
            $companyId,
            (string) ($record['payment_account_name'] ?? ''),
            (string) ($record['payment_method'] ?? '')
        ) ?? '',
        'asset',
        ['1112', '1111', '1100']
    );
    $number = app_normalize_whitespace((string) ($record[$numberField] ?? ''));
    $description = app_normalize_whitespace((string) ($record['description'] ?? $record['notes'] ?? ''));
    $memo = trim($dateLabel . ' ' . $number . ($description !== '' ? ' - ' . $description : ''));
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));

    return [
        app_accounting_line($expenseCode, $amount, 0, $memo, $costCenterCode),
        app_accounting_line($paymentCode, 0, $amount, $memo . ' paid'),
    ];
}

function app_build_prepaid_expense_journal_lines(int $companyId, array $record): array
{
    $totalAmount = max(0, app_number($record['total_amount'] ?? 0));
    $taxTotal = max(0, app_number($record['tax_total'] ?? 0));
    $grandTotal = max(0, app_number($record['grand_total'] ?? 0));
    if ($grandTotal <= 0) {
        $grandTotal = round($totalAmount + $taxTotal, 2);
    }
    if ($taxTotal > $grandTotal) {
        $taxTotal = $grandTotal;
    }
    if ($totalAmount <= 0 && $taxTotal <= 0) {
        return [];
    }

    $paidAmount = max(0, app_number($record['inline_paid_amount'] ?? 0));
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }
    $unpaidAmount = max(0, round($grandTotal - $paidAmount, 2));

    $prepaidCode = app_resolve_account_code(
        $companyId,
        (string) ($record['prepaid_account_code'] ?? ''),
        'asset',
        ['1150', '1100']
    );
    $taxInputCode = '';
    if ($taxTotal > 0) {
        $taxInputCode = app_tax_posting_account_code($companyId, (string) ($record['tax_name'] ?? ''), 'purchase')
            ?? app_resolve_account_code($companyId, '', 'asset', ['1140']);
    }
    $accruedCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
    $paymentAccountCode = '';
    if ($paidAmount > 0) {
        $paymentAccountCode = app_resolve_account_code(
            $companyId,
            app_resolve_payment_account_gl_account_code(
                $companyId,
                (string) ($record['payment_account_name'] ?? ''),
                (string) ($record['payment_method'] ?? '')
            ) ?? '',
            'asset',
            ['1112', '1111', '1100']
        );
    }

    $prepaidNumber = app_normalize_whitespace((string) ($record['prepaid_number'] ?? ''));
    $description = app_normalize_whitespace((string) ($record['description'] ?? ''));
    if ($description === '') {
        $description = 'Prepaid expense ' . $prepaidNumber;
    }

    $lines = [];
    if ($totalAmount > 0) {
        $lines[] = app_accounting_line($prepaidCode, $totalAmount, 0, $description);
    }
    if ($taxTotal > 0) {
        $lines[] = app_accounting_line($taxInputCode, $taxTotal, 0, 'Prepaid expense ' . $prepaidNumber . ' input VAT');
    }
    if ($paidAmount > 0) {
        $lines[] = app_accounting_line($paymentAccountCode, 0, $paidAmount, 'Payment for prepaid expense ' . $prepaidNumber);
    }
    if ($unpaidAmount > 0) {
        $lines[] = app_accounting_line($accruedCode, 0, $unpaidAmount, 'Accrued prepaid expense ' . $prepaidNumber);
    }

    return $lines;
}

function app_build_prepaid_amortization_journal_lines(int $companyId, array $record): array
{
    $amount = max(0, app_number($record['amount'] ?? 0));
    if ($amount <= 0) {
        return [];
    }

    $expenseCode = app_resolve_account_code(
        $companyId,
        (string) ($record['expense_account_code'] ?? ''),
        'expense',
        ['5310', '5210', '5510']
    );
    $prepaidCode = app_resolve_account_code(
        $companyId,
        (string) ($record['prepaid_account_code'] ?? ''),
        'asset',
        ['1150', '1100']
    );
    $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
    $runNumber = app_normalize_whitespace((string) ($record['run_number'] ?? ''));
    $prepaidNumber = app_normalize_whitespace((string) ($record['prepaid_number'] ?? ''));
    $memo = 'Prepaid amortization ' . ($runNumber !== '' ? $runNumber : $prepaidNumber);

    return [
        app_accounting_line($expenseCode, $amount, 0, $memo, $costCenterCode),
        app_accounting_line($prepaidCode, 0, $amount, $memo),
    ];
}

function app_build_payment_journal_lines(int $companyId, array $record): array
{
    $amount = max(0, app_number($record['amount'] ?? 0));
    if ($amount <= 0) {
        return [];
    }

    $direction = strtolower(app_normalize_whitespace((string) ($record['direction'] ?? 'in')));
    $reference = app_normalize_whitespace((string) ($record['reference'] ?? ''));
    $paymentNumber = app_normalize_whitespace((string) ($record['payment_number'] ?? ''));
    $linkedSalesInvoice = null;
    $linkedPurchaseBill = null;
    $linkedExpense = null;
    $linkedIfrs16Lease = null;
    $linkedIfrs16Schedule = null;
    $salesInvoiceId = app_integer($record['sales_invoice_id'] ?? 0);
    if ($salesInvoiceId > 0 && $direction === 'in') {
        $linkedSalesInvoice = app_sales_invoice_by_id($companyId, $salesInvoiceId);
        if ($linkedSalesInvoice) {
            $reference = app_normalize_whitespace((string) ($linkedSalesInvoice['invoice_number'] ?? $reference));
        }
    }
    $purchaseBillId = app_integer($record['purchase_bill_id'] ?? 0);
    if ($purchaseBillId > 0 && $direction === 'out') {
        $linkedPurchaseBill = app_purchase_bill_by_id($companyId, $purchaseBillId);
        if ($linkedPurchaseBill) {
            $reference = app_normalize_whitespace((string) ($linkedPurchaseBill['bill_number'] ?? $reference));
        }
    }
    $expenseId = app_integer($record['expense_id'] ?? 0);
    if ($expenseId > 0 && $direction === 'out') {
        $linkedExpense = app_expense_by_id($companyId, $expenseId);
        if ($linkedExpense) {
            $reference = app_normalize_whitespace((string) ($linkedExpense['expense_number'] ?? $reference));
        }
    }
    $ifrs16ScheduleId = app_integer($record['ifrs16_schedule_id'] ?? 0);
    if ($ifrs16ScheduleId > 0 && $direction === 'out') {
        $linkedIfrs16Schedule = app_ifrs16_schedule_by_id($companyId, $ifrs16ScheduleId);
        if ($linkedIfrs16Schedule) {
            $linkedIfrs16Lease = app_ifrs16_lease_by_id($companyId, app_integer($linkedIfrs16Schedule['lease_id'] ?? 0));
            $leaseNumber = app_normalize_whitespace((string) ($linkedIfrs16Schedule['lease_number'] ?? ($linkedIfrs16Lease['lease_number'] ?? '')));
            if ($leaseNumber !== '' && $reference === '') {
                $reference = $leaseNumber;
            }
        }
    }
    $ifrs16LeaseId = app_integer($record['ifrs16_lease_id'] ?? 0);
    if (!$linkedIfrs16Lease && $ifrs16LeaseId > 0 && $direction === 'out') {
        $linkedIfrs16Lease = app_ifrs16_lease_by_id($companyId, $ifrs16LeaseId);
        if ($linkedIfrs16Lease && $reference === '') {
            $reference = app_normalize_whitespace((string) ($linkedIfrs16Lease['lease_number'] ?? $reference));
        }
    }

    $bankCode = app_resolve_payment_account_gl_account_code(
        $companyId,
        (string) ($record['bank_account_name'] ?? ''),
        (string) ($record['payment_method'] ?? '')
    );
    $cashOrBankCode = app_resolve_account_code($companyId, $bankCode ?? '', 'asset', ['1112', '1111', '1100']);

    $offsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
    if ($linkedSalesInvoice && $direction === 'in') {
        $offsetCode = $offsetCode !== ''
            ? app_resolve_account_code($companyId, $offsetCode, 'asset', ['1121', '1120', '1200'])
            : app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
    } elseif ($linkedPurchaseBill && $direction === 'out') {
        $offsetCode = app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100']);
    } elseif ($linkedExpense && $direction === 'out') {
        $linkedExpenseOffset = app_normalize_whitespace((string) ($linkedExpense['offset_account_code'] ?? $offsetCode));
        if ($linkedExpenseOffset === '') {
            $linkedExpenseOffset = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
        }
        $offsetCode = app_resolve_account_code(
            $companyId,
            $linkedExpenseOffset,
            'liability',
            ['2130', '2111', '2100']
        );
    } elseif ($linkedIfrs16Lease && $direction === 'out') {
        $offsetCode = app_resolve_account_code(
            $companyId,
            (string) ($linkedIfrs16Lease['lease_liability_account_code'] ?? $offsetCode),
            'liability',
            ['2210', '2200', '2130']
        );
    } elseif ($offsetCode !== '') {
        app_assert_leaf_account($companyId, $offsetCode, null, 'Offset account');
        // Keep explicitly selected offset code.
    } else {
        $pdo = app_pdo();
        $offsetCode = '';
        if ($reference !== '') {
            if ($direction === 'in') {
                $stmt = $pdo->prepare(
                    'SELECT invoice_number FROM sales_invoices WHERE company_id = :company_id AND invoice_number = :reference LIMIT 1'
                );
                $stmt->execute(['company_id' => $companyId, 'reference' => $reference]);
                if ($stmt->fetchColumn()) {
                    $offsetCode = app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
                }
            } elseif ($direction === 'out') {
                $stmt = $pdo->prepare(
                    'SELECT bill_number FROM purchase_bills WHERE company_id = :company_id AND bill_number = :reference LIMIT 1'
                );
                $stmt->execute(['company_id' => $companyId, 'reference' => $reference]);
                if ($stmt->fetchColumn()) {
                    $offsetCode = app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100']);
                }
                if ($offsetCode === '') {
                    $stmt = $pdo->prepare(
                        'SELECT expense_number FROM expenses WHERE company_id = :company_id AND expense_number = :reference LIMIT 1'
                    );
                    $stmt->execute(['company_id' => $companyId, 'reference' => $reference]);
                    if ($stmt->fetchColumn()) {
                        $offsetCode = app_posting_account_code($companyId, 'accrued_expense', 'liability', ['2130', '2111', '2100']);
                    }
                }
            }
        }

        if ($offsetCode === '') {
            $offsetCode = $direction === 'out'
                ? app_posting_account_code($companyId, 'purchase_payable', 'liability', ['2111', '2110', '2100'])
                : app_posting_account_code($companyId, 'sales_receivable', 'asset', ['1121', '1120', '1200']);
        }
    }

    $lines = [];
    $offsetMemo = $reference !== ''
        ? 'Payment ' . $paymentNumber . ' for ' . $reference
        : 'Payment ' . $paymentNumber . ' offset';
    if ($direction === 'out') {
        $lines[] = app_accounting_line($offsetCode, $amount, 0, $offsetMemo);
        $lines[] = app_accounting_line($cashOrBankCode, 0, $amount, 'Payment ' . $paymentNumber . ' cash/bank');
    } else {
        $lines[] = app_accounting_line($cashOrBankCode, $amount, 0, 'Payment ' . $paymentNumber . ' cash/bank');
        $lines[] = app_accounting_line($offsetCode, 0, $amount, $offsetMemo);
    }

    return $lines;
}

function app_pos_payment_methods(): array
{
    return [
        'cash' => 'Cash',
        'card' => 'Card',
        'bank' => 'Bank',
        'transfer' => 'Transfer',
    ];
}

function app_next_prefixed_number(int $companyId, string $table, string $field, string $prefix): string
{
    if ($companyId <= 0 || !app_identifier($table) || !app_identifier($field)) {
        throw new RuntimeException('Invalid numbering context.');
    }

    $prefix = app_normalize_whitespace($prefix);
    if ($prefix === '') {
        throw new RuntimeException('Invalid numbering prefix.');
    }

    $stmt = app_pdo()->prepare(
        'SELECT `' . $field . '` AS doc_no
         FROM `' . $table . '`
         WHERE company_id = :company_id
           AND `' . $field . '` LIKE :prefix
         ORDER BY id DESC
         LIMIT 500'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'prefix' => $prefix . '-%',
    ]);

    $max = 0;
    $pattern = '/^' . preg_quote($prefix, '/') . '-(\d+)$/i';
    foreach (($stmt->fetchAll() ?: []) as $row) {
        $docNo = app_normalize_whitespace((string) ($row['doc_no'] ?? ''));
        if (preg_match($pattern, $docNo, $matches)) {
            $max = max($max, (int) ($matches[1] ?? 0));
        }
    }

    return $prefix . '-' . str_pad((string) ($max + 1), 6, '0', STR_PAD_LEFT);
}

function app_pos_register_by_id(int $companyId, int $registerId): ?array
{
    if ($companyId <= 0 || $registerId <= 0) {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM pos_registers
         WHERE company_id = :company_id
           AND id = :id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $registerId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_pos_registers(int $companyId, bool $onlyActive = true): array
{
    if ($companyId <= 0) {
        return [];
    }
    $sql = 'SELECT * FROM pos_registers WHERE company_id = :company_id';
    if ($onlyActive) {
        $sql .= ' AND is_active = 1';
    }
    $sql .= ' ORDER BY register_code ASC, id ASC';
    $stmt = app_pdo()->prepare($sql);
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_pos_session_by_id(int $companyId, int $sessionId): ?array
{
    if ($companyId <= 0 || $sessionId <= 0) {
        return null;
    }
    $stmt = app_pdo()->prepare(
        'SELECT s.*, r.register_code, r.register_name, r.cash_account_name, r.default_bank_account_name,
                r.default_customer_code, r.cost_center_code, r.sales_revenue_account_code,
                r.price_includes_tax, r.allow_negative_stock, r.receipt_footer
         FROM pos_sessions s
         INNER JOIN pos_registers r
                 ON r.company_id = s.company_id
                AND r.id = s.register_id
         WHERE s.company_id = :company_id
           AND s.id = :id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $sessionId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_pos_open_sessions(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        "SELECT s.*, r.register_code, r.register_name, r.cash_account_name, r.default_bank_account_name,
                r.default_customer_code, r.cost_center_code, r.price_includes_tax, r.allow_negative_stock
         FROM pos_sessions s
         INNER JOIN pos_registers r
                 ON r.company_id = s.company_id
                AND r.id = s.register_id
         WHERE s.company_id = :company_id
           AND s.status = 'open'
         ORDER BY s.opened_at DESC, s.id DESC"
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_pos_session_summary(int $companyId, int $sessionId): array
{
    if ($companyId <= 0 || $sessionId <= 0) {
        return [
            'orders_count' => 0,
            'sales_total' => 0.0,
            'refund_total' => 0.0,
            'net_total' => 0.0,
            'cash_payments' => 0.0,
            'cash_in' => 0.0,
            'cash_out' => 0.0,
            'expected_cash' => 0.0,
        ];
    }

    $session = app_pos_session_by_id($companyId, $sessionId);
    $opening = max(0, app_number($session['opening_cash_counted'] ?? 0));
    $pdo = app_pdo();

    $orderStmt = $pdo->prepare(
        "SELECT
            COUNT(*) AS orders_count,
            COALESCE(SUM(CASE WHEN grand_total >= 0 THEN grand_total ELSE 0 END), 0) AS sales_total,
            COALESCE(SUM(CASE WHEN grand_total < 0 THEN ABS(grand_total) ELSE 0 END), 0) AS refund_total,
            COALESCE(SUM(grand_total), 0) AS net_total
         FROM pos_orders
         WHERE company_id = :company_id
           AND session_id = :session_id
           AND status <> 'cancelled'"
    );
    $orderStmt->execute(['company_id' => $companyId, 'session_id' => $sessionId]);
    $orders = $orderStmt->fetch() ?: [];

    $cashPaymentStmt = $pdo->prepare(
        "SELECT COALESCE(SUM(p.amount), 0)
         FROM pos_payments p
         INNER JOIN pos_orders o
                 ON o.company_id = p.company_id
                AND o.id = p.order_id
         WHERE p.company_id = :company_id
           AND o.session_id = :session_id
           AND o.status <> 'cancelled'
           AND p.payment_method = 'cash'"
    );
    $cashPaymentStmt->execute(['company_id' => $companyId, 'session_id' => $sessionId]);
    $cashPayments = round(app_number($cashPaymentStmt->fetchColumn() ?: 0), 2);

    $movementStmt = $pdo->prepare(
        "SELECT
            COALESCE(SUM(CASE WHEN movement_type = 'cash_in' THEN amount ELSE 0 END), 0) AS cash_in,
            COALESCE(SUM(CASE WHEN movement_type = 'cash_out' THEN amount ELSE 0 END), 0) AS cash_out
         FROM pos_cash_movements
         WHERE company_id = :company_id
           AND session_id = :session_id
           AND status <> 'cancelled'"
    );
    $movementStmt->execute(['company_id' => $companyId, 'session_id' => $sessionId]);
    $movements = $movementStmt->fetch() ?: [];
    $cashIn = round(max(0, app_number($movements['cash_in'] ?? 0)), 2);
    $cashOut = round(max(0, app_number($movements['cash_out'] ?? 0)), 2);
    $expectedCash = round($opening + $cashPayments + $cashIn - $cashOut, 2);

    return [
        'orders_count' => app_integer($orders['orders_count'] ?? 0),
        'sales_total' => round(max(0, app_number($orders['sales_total'] ?? 0)), 2),
        'refund_total' => round(max(0, app_number($orders['refund_total'] ?? 0)), 2),
        'net_total' => round(app_number($orders['net_total'] ?? 0), 2),
        'cash_payments' => $cashPayments,
        'cash_in' => $cashIn,
        'cash_out' => $cashOut,
        'expected_cash' => $expectedCash,
    ];
}

function app_pos_sync_session_expected_cash(int $companyId, int $sessionId): array
{
    $summary = app_pos_session_summary($companyId, $sessionId);
    app_pdo()->prepare(
        "UPDATE pos_sessions
         SET expected_cash_amount = :expected_cash
         WHERE company_id = :company_id
           AND id = :id
           AND status = 'open'"
    )->execute([
        'expected_cash' => (float) ($summary['expected_cash'] ?? 0),
        'company_id' => $companyId,
        'id' => $sessionId,
    ]);
    return $summary;
}

function app_pos_catalog_products(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        'SELECT sku, product_name, category, unit_name, sale_price, purchase_price, qty_on_hand, tax_rate_percent
         FROM products
         WHERE company_id = :company_id
           AND is_active = 1
         ORDER BY category ASC, product_name ASC, sku ASC'
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_pos_order_lines(int $companyId, int $orderId): array
{
    if ($companyId <= 0 || $orderId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM pos_order_lines
         WHERE company_id = :company_id
           AND order_id = :order_id
         ORDER BY line_no ASC, id ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'order_id' => $orderId]);
    return $stmt->fetchAll() ?: [];
}

function app_pos_order_payments(int $companyId, int $orderId): array
{
    if ($companyId <= 0 || $orderId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM pos_payments
         WHERE company_id = :company_id
           AND order_id = :order_id
         ORDER BY line_no ASC, id ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'order_id' => $orderId]);
    return $stmt->fetchAll() ?: [];
}

function app_pos_open_session(int $companyId, int $registerId, float $openingCash): int
{
    $register = app_pos_register_by_id($companyId, $registerId);
    if (!$register || (int) ($register['is_active'] ?? 0) !== 1) {
        throw new RuntimeException('Select an active POS register.');
    }
    app_assert_payment_account_postable($companyId, (string) ($register['cash_account_name'] ?? ''), 'POS cash drawer', 'cash');
    $costCenterCode = app_normalize_whitespace((string) ($register['cost_center_code'] ?? ''));
    if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
        throw new RuntimeException('POS register needs a valid cost center before opening a session.');
    }

    $existingStmt = app_pdo()->prepare(
        "SELECT id
         FROM pos_sessions
         WHERE company_id = :company_id
           AND register_id = :register_id
           AND status = 'open'
         LIMIT 1"
    );
    $existingStmt->execute(['company_id' => $companyId, 'register_id' => $registerId]);
    $existingId = (int) ($existingStmt->fetchColumn() ?: 0);
    if ($existingId > 0) {
        return $existingId;
    }

    $sessionNumber = app_next_prefixed_number($companyId, 'pos_sessions', 'session_number', 'POSS');
    $stmt = app_pdo()->prepare(
        'INSERT INTO pos_sessions
         (company_id, register_id, session_number, opened_at, opening_cash_counted, expected_cash_amount, status, opened_by_user_id)
         VALUES
         (:company_id, :register_id, :session_number, :opened_at, :opening_cash_counted, :expected_cash_amount, :status, :opened_by_user_id)'
    );
    $openingCash = round(max(0, $openingCash), 2);
    $stmt->execute([
        'company_id' => $companyId,
        'register_id' => $registerId,
        'session_number' => $sessionNumber,
        'opened_at' => date('Y-m-d H:i:s'),
        'opening_cash_counted' => $openingCash,
        'expected_cash_amount' => $openingCash,
        'status' => 'open',
        'opened_by_user_id' => app_current_user()['id'] ?? null,
    ]);
    return (int) app_pdo()->lastInsertId();
}

function app_build_pos_order_journal_lines(int $companyId, array $record): array
{
    $orderId = (int) ($record['id'] ?? 0);
    $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'paid')));
    if ($companyId <= 0 || $orderId <= 0 || $status === 'cancelled') {
        return [];
    }

    $lines = [];
    $orderNumber = app_normalize_whitespace((string) ($record['order_number'] ?? ''));
    foreach (app_pos_order_payments($companyId, $orderId) as $payment) {
        $amount = round(app_number($payment['amount'] ?? 0), 2);
        if (abs($amount) < 0.01) {
            continue;
        }
        $method = app_normalize_whitespace((string) ($payment['payment_method'] ?? 'cash'));
        $accountName = app_normalize_whitespace((string) ($payment['payment_account_name'] ?? ''));
        $accountCode = app_resolve_payment_account_gl_account_code($companyId, $accountName, $method);
        $accountCode = app_resolve_account_code($companyId, $accountCode ?? '', 'asset', ['1111', '1112', '1100']);
        $memo = 'POS payment ' . $orderNumber . ' - ' . $accountName;
        $lines[] = $amount >= 0
            ? app_accounting_line($accountCode, abs($amount), 0, $memo)
            : app_accounting_line($accountCode, 0, abs($amount), $memo);
    }

    foreach (app_pos_order_lines($companyId, $orderId) as $orderLine) {
        $base = round(app_number($orderLine['subtotal'] ?? 0), 2);
        $tax = round(app_number($orderLine['tax_total'] ?? 0), 2);
        $revenueCode = app_resolve_account_code(
            $companyId,
            (string) ($orderLine['revenue_account_code'] ?? ''),
            'revenue',
            ['4100', '4110', '4000']
        );
        $costCenterCode = app_normalize_whitespace((string) ($orderLine['cost_center_code'] ?? $record['cost_center_code'] ?? ''));
        $lineMemo = 'POS sale ' . $orderNumber . ' - ' . app_normalize_whitespace((string) ($orderLine['product_name'] ?? $orderLine['sku'] ?? 'Item'));
        if (abs($base) >= 0.01) {
            $lines[] = $base >= 0
                ? app_accounting_line($revenueCode, 0, abs($base), $lineMemo, $costCenterCode)
                : app_accounting_line($revenueCode, abs($base), 0, 'POS refund ' . $orderNumber, $costCenterCode);
        }
        if (abs($tax) >= 0.01) {
            $taxCode = app_tax_posting_account_code($companyId, (string) ($orderLine['tax_name'] ?? ''), 'sales');
            if ($taxCode === null) {
                $taxCode = app_resolve_account_code($companyId, '', 'liability', ['2120', '2100']);
            }
            $lines[] = $tax >= 0
                ? app_accounting_line($taxCode, 0, abs($tax), 'POS output tax ' . $orderNumber)
                : app_accounting_line($taxCode, abs($tax), 0, 'POS output tax refund ' . $orderNumber);
        }
    }

    return $lines;
}

function app_build_pos_cash_movement_journal_lines(int $companyId, array $record): array
{
    $amount = round(max(0, app_number($record['amount'] ?? 0)), 2);
    $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'posted')));
    if ($amount <= 0 || $status === 'cancelled') {
        return [];
    }

    $session = app_pos_session_by_id($companyId, app_integer($record['session_id'] ?? 0));
    $costCenterCode = app_normalize_whitespace((string) ($session['cost_center_code'] ?? ''));
    $cashCode = app_resolve_payment_account_gl_account_code($companyId, (string) ($record['payment_account_name'] ?? ''), 'cash');
    $cashCode = app_resolve_account_code($companyId, $cashCode ?? '', 'asset', ['1111', '1112', '1100']);
    $offsetCode = app_normalize_whitespace((string) ($record['offset_account_code'] ?? ''));
    app_assert_leaf_account($companyId, $offsetCode, null, 'POS cash movement offset account');
    $movementNumber = app_normalize_whitespace((string) ($record['movement_number'] ?? ''));
    $movementType = app_normalize_whitespace((string) ($record['movement_type'] ?? 'cash_in'));
    $memo = 'POS ' . ($movementType === 'cash_out' ? 'cash out ' : 'cash in ') . $movementNumber;

    if ($movementType === 'cash_out') {
        return [
            app_accounting_line($offsetCode, $amount, 0, $memo, $costCenterCode),
            app_accounting_line($cashCode, 0, $amount, $memo),
        ];
    }

    return [
        app_accounting_line($cashCode, $amount, 0, $memo),
        app_accounting_line($offsetCode, 0, $amount, $memo, $costCenterCode),
    ];
}

function app_build_pos_session_journal_lines(int $companyId, array $record): array
{
    $difference = round(app_number($record['cash_difference'] ?? 0), 2);
    $status = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'open')));
    if ($companyId <= 0 || $status !== 'closed' || abs($difference) < 0.01) {
        return [];
    }

    $register = app_pos_register_by_id($companyId, app_integer($record['register_id'] ?? 0));
    $cashAccountName = app_normalize_whitespace((string) ($register['cash_account_name'] ?? $record['cash_account_name'] ?? ''));
    $cashCode = app_resolve_payment_account_gl_account_code($companyId, $cashAccountName, 'cash');
    $cashCode = app_resolve_account_code($companyId, $cashCode ?? '', 'asset', ['1111', '1112', '1100']);
    $costCenterCode = app_normalize_whitespace((string) ($register['cost_center_code'] ?? $record['cost_center_code'] ?? ''));
    $sessionNumber = app_normalize_whitespace((string) ($record['session_number'] ?? ''));

    if ($difference > 0) {
        $gainCode = app_posting_account_code($companyId, 'pos_cash_over_short_gain', 'revenue', ['4100', '4000']);
        return [
            app_accounting_line($cashCode, abs($difference), 0, 'POS cash overage ' . $sessionNumber),
            app_accounting_line($gainCode, 0, abs($difference), 'POS cash overage ' . $sessionNumber, $costCenterCode),
        ];
    }

    $lossCode = app_posting_account_code($companyId, 'pos_cash_over_short_loss', 'expense', ['5510', '5300', '5100']);
    return [
        app_accounting_line($lossCode, abs($difference), 0, 'POS cash shortage ' . $sessionNumber, $costCenterCode),
        app_accounting_line($cashCode, 0, abs($difference), 'POS cash shortage ' . $sessionNumber),
    ];
}

function app_pos_create_cash_movement(int $companyId, int $sessionId, array $input): int
{
    $session = app_pos_session_by_id($companyId, $sessionId);
    if (!$session || strtolower(app_normalize_whitespace((string) ($session['status'] ?? ''))) !== 'open') {
        throw new RuntimeException('Select an open POS session.');
    }

    $movementType = app_normalize_whitespace((string) ($input['movement_type'] ?? 'cash_in'));
    if (!in_array($movementType, ['cash_in', 'cash_out'], true)) {
        throw new RuntimeException('Select a valid cash movement type.');
    }
    $amount = round(max(0, app_number($input['amount'] ?? 0)), 2);
    if ($amount <= 0) {
        throw new RuntimeException('Cash movement amount must be greater than zero.');
    }
    $paymentAccountName = app_normalize_whitespace((string) ($session['cash_account_name'] ?? ''));
    app_assert_payment_account_postable($companyId, $paymentAccountName, 'POS cash drawer', 'cash');
    $offsetCode = app_normalize_whitespace((string) ($input['offset_account_code'] ?? ''));
    app_assert_leaf_account($companyId, $offsetCode, null, 'Cash movement offset account');

    $pdo = app_pdo();
    $movementNumber = app_next_prefixed_number($companyId, 'pos_cash_movements', 'movement_number', 'POSC');
    $stmt = $pdo->prepare(
        'INSERT INTO pos_cash_movements
         (company_id, session_id, movement_number, movement_date, movement_type, payment_account_name, offset_account_code, amount, reason, status, created_by_user_id)
         VALUES
         (:company_id, :session_id, :movement_number, :movement_date, :movement_type, :payment_account_name, :offset_account_code, :amount, :reason, :status, :created_by_user_id)'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'session_id' => $sessionId,
        'movement_number' => $movementNumber,
        'movement_date' => date('Y-m-d H:i:s'),
        'movement_type' => $movementType,
        'payment_account_name' => $paymentAccountName,
        'offset_account_code' => $offsetCode,
        'amount' => $amount,
        'reason' => app_normalize_whitespace((string) ($input['reason'] ?? '')),
        'status' => 'posted',
        'created_by_user_id' => app_current_user()['id'] ?? null,
    ]);
    $movementId = (int) $pdo->lastInsertId();
    $record = app_module_find('pos_cash_movements', $movementId, $companyId);
    if ($record) {
        app_upsert_auto_journal_entry(
            $companyId,
            'pos_cash_movements',
            $movementId,
            substr((string) ($record['movement_date'] ?? date('Y-m-d')), 0, 10),
            (string) ($record['movement_number'] ?? $movementNumber),
            'Auto posting for POS cash movement ' . (string) ($record['movement_number'] ?? $movementNumber),
            app_build_pos_cash_movement_journal_lines($companyId, $record),
            'posted'
        );
    }
    app_pos_sync_session_expected_cash($companyId, $sessionId);
    return $movementId;
}

function app_pos_create_order(int $companyId, array $input): int
{
    $sessionId = app_integer($input['session_id'] ?? 0);
    $session = app_pos_session_by_id($companyId, $sessionId);
    if (!$session || strtolower(app_normalize_whitespace((string) ($session['status'] ?? ''))) !== 'open') {
        throw new RuntimeException('Select an open POS session before validating orders.');
    }

    $orderType = app_normalize_whitespace((string) ($input['order_type'] ?? 'sale'));
    if (!in_array($orderType, ['sale', 'refund'], true)) {
        throw new RuntimeException('Invalid POS order type.');
    }
    $sign = $orderType === 'refund' ? -1 : 1;
    $cart = json_decode((string) ($input['cart_json'] ?? '[]'), true);
    $payments = json_decode((string) ($input['payments_json'] ?? '[]'), true);
    if (!is_array($cart) || !$cart) {
        throw new RuntimeException('Add at least one product to the POS order.');
    }
    if (!is_array($payments)) {
        $payments = [];
    }

    $settings = app_posting_account_settings($companyId);
    $defaultRevenueCode = app_normalize_whitespace((string) ($session['sales_revenue_account_code'] ?? ''));
    if ($defaultRevenueCode === '') {
        $defaultRevenueCode = app_normalize_whitespace((string) ($settings['pos_sales_revenue_account_code'] ?? ''));
    }
    $defaultRevenueCode = app_resolve_account_code($companyId, $defaultRevenueCode, 'revenue', ['4100', '4110', '4000']);
    $costCenterCode = app_normalize_whitespace((string) ($session['cost_center_code'] ?? ''));
    if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
        throw new RuntimeException('POS session register needs a valid cost center.');
    }
    $customerCode = app_normalize_whitespace((string) ($input['customer_code'] ?? ''));
    if ($customerCode === '') {
        $customerCode = app_normalize_whitespace((string) ($session['default_customer_code'] ?? ''));
    }
    if ($customerCode !== '' && !app_contact_exists($companyId, $customerCode, 'customer')) {
        throw new RuntimeException('Select a valid POS customer.');
    }

    $pdo = app_pdo();
    $skuList = [];
    foreach ($cart as $line) {
        $sku = app_normalize_whitespace((string) ($line['sku'] ?? ''));
        if ($sku !== '') {
            $skuList[$sku] = true;
        }
    }
    if (!$skuList) {
        throw new RuntimeException('POS order lines must contain valid products.');
    }

    $productRows = [];
    $placeholders = [];
    $params = ['company_id' => $companyId];
    foreach (array_keys($skuList) as $index => $sku) {
        $key = 'sku_' . $index;
        $placeholders[] = ':' . $key;
        $params[$key] = $sku;
    }
    $productStmt = $pdo->prepare(
        'SELECT *
         FROM products
         WHERE company_id = :company_id
           AND sku IN (' . implode(',', $placeholders) . ')'
    );
    $productStmt->execute($params);
    foreach (($productStmt->fetchAll() ?: []) as $product) {
        $productRows[app_normalize_whitespace((string) ($product['sku'] ?? ''))] = $product;
    }

    $priceIncludesTax = (int) ($session['price_includes_tax'] ?? 0) === 1;
    $allowNegativeStock = (int) ($session['allow_negative_stock'] ?? 0) === 1;
    $taxName = '';
    $salesTaxes = app_tax_rate_map($companyId, 'sales');
    if ($salesTaxes) {
        $taxName = (string) array_key_first($salesTaxes);
    }
    $normalizedLines = [];
    $subtotal = 0.0;
    $discountTotal = 0.0;
    $taxTotal = 0.0;
    $grandTotal = 0.0;

    foreach ($cart as $line) {
        $sku = app_normalize_whitespace((string) ($line['sku'] ?? ''));
        $product = $productRows[$sku] ?? null;
        if (!$product) {
            throw new RuntimeException('Product not found in POS catalog: ' . $sku);
        }
        $quantity = round(abs(app_number($line['quantity'] ?? 0)), 3);
        if ($quantity <= 0) {
            continue;
        }
        $signedQuantity = $quantity * $sign;
        $unitPrice = round(max(0, app_number($line['unit_price'] ?? $product['sale_price'] ?? 0)), 2);
        $discountPercent = min(100, max(0, app_number($line['discount_percent'] ?? 0)));
        $taxRate = max(0, app_number($line['tax_rate_percent'] ?? $product['tax_rate_percent'] ?? 0));
        $gross = round($quantity * $unitPrice, 2);
        $lineDiscount = round($gross * ($discountPercent / 100), 2);
        $afterDiscount = round($gross - $lineDiscount, 2);
        if ($priceIncludesTax && $taxRate > 0) {
            $lineBaseAbs = round($afterDiscount / (1 + ($taxRate / 100)), 2);
            $lineTaxAbs = round($afterDiscount - $lineBaseAbs, 2);
        } else {
            $lineBaseAbs = $afterDiscount;
            $lineTaxAbs = round($lineBaseAbs * ($taxRate / 100), 2);
        }
        $lineTotalAbs = round($lineBaseAbs + $lineTaxAbs, 2);
        $currentQty = app_number($product['qty_on_hand'] ?? 0);
        $newQty = round($currentQty - $signedQuantity, 3);
        if (!$allowNegativeStock && $newQty < -0.0001) {
            throw new RuntimeException('Insufficient stock for product ' . $sku . '. Available: ' . app_money_format($currentQty));
        }
        $lineTaxName = $taxRate > 0 ? $taxName : '';
        $normalizedLines[] = [
            'sku' => $sku,
            'product_name' => app_normalize_whitespace((string) ($product['product_name'] ?? $sku)),
            'quantity' => $signedQuantity,
            'unit_price' => $unitPrice,
            'discount_percent' => $discountPercent,
            'discount_amount' => round($lineDiscount * $sign, 2),
            'tax_rate_percent' => $taxRate,
            'tax_name' => $lineTaxName,
            'subtotal' => round($lineBaseAbs * $sign, 2),
            'tax_total' => round($lineTaxAbs * $sign, 2),
            'total' => round($lineTotalAbs * $sign, 2),
            'revenue_account_code' => $defaultRevenueCode,
            'cost_center_code' => $costCenterCode,
            'customer_note' => app_normalize_whitespace((string) ($line['customer_note'] ?? '')),
            'stock_after' => $newQty,
            'purchase_price' => max(0, app_number($product['purchase_price'] ?? 0)),
        ];
        $subtotal += $lineBaseAbs * $sign;
        $discountTotal += $lineDiscount * $sign;
        $taxTotal += $lineTaxAbs * $sign;
        $grandTotal += $lineTotalAbs * $sign;
    }

    if (!$normalizedLines) {
        throw new RuntimeException('Add at least one valid product quantity.');
    }

    $subtotal = round($subtotal, 2);
    $discountTotal = round($discountTotal, 2);
    $taxTotal = round($taxTotal, 2);
    $grandTotal = round($grandTotal, 2);
    $normalizedPayments = [];
    $paymentTotal = 0.0;
    foreach ($payments as $payment) {
        $method = app_normalize_whitespace((string) ($payment['payment_method'] ?? 'cash'));
        if (!array_key_exists($method, app_pos_payment_methods())) {
            throw new RuntimeException('Select a valid POS payment method.');
        }
        $amount = round(abs(app_number($payment['amount'] ?? 0)), 2);
        if ($amount <= 0) {
            continue;
        }
        $accountName = app_normalize_whitespace((string) ($payment['payment_account_name'] ?? ''));
        if ($accountName === '') {
            $accountName = $method === 'cash'
                ? app_normalize_whitespace((string) ($session['cash_account_name'] ?? ''))
                : app_normalize_whitespace((string) ($session['default_bank_account_name'] ?? ''));
        }
        app_assert_payment_account_postable($companyId, $accountName, 'POS payment account', $method);
        $signedAmount = $orderType === 'refund' ? -$amount : $amount;
        $normalizedPayments[] = [
            'payment_method' => $method,
            'payment_account_name' => $accountName,
            'amount' => $signedAmount,
            'reference' => app_normalize_whitespace((string) ($payment['reference'] ?? '')),
        ];
        $paymentTotal += $signedAmount;
    }

    if (!$normalizedPayments && abs($grandTotal) >= 0.01) {
        throw new RuntimeException('Add at least one POS payment line.');
    }

    $changeAmount = 0.0;
    if ($orderType === 'sale') {
        if ($paymentTotal + 0.01 < $grandTotal) {
            throw new RuntimeException('POS payments are less than the order total.');
        }
        $changeAmount = round(max(0, $paymentTotal - $grandTotal), 2);
        if ($changeAmount > 0) {
            for ($i = count($normalizedPayments) - 1; $i >= 0; $i--) {
                if (($normalizedPayments[$i]['payment_method'] ?? '') === 'cash' && $normalizedPayments[$i]['amount'] >= $changeAmount) {
                    $normalizedPayments[$i]['amount'] = round($normalizedPayments[$i]['amount'] - $changeAmount, 2);
                    $paymentTotal = round($paymentTotal - $changeAmount, 2);
                    break;
                }
            }
        }
        if (abs($paymentTotal - $grandTotal) >= 0.01) {
            throw new RuntimeException('POS payments must equal the order total after change.');
        }
    } elseif (abs($paymentTotal - $grandTotal) >= 0.01) {
        throw new RuntimeException('Refund payments must equal the refund total.');
    }

    $pdo->beginTransaction();
    try {
        $orderNumber = app_next_prefixed_number($companyId, 'pos_orders', 'order_number', 'POSO');
        $receiptNumber = 'RCP-' . substr($orderNumber, 5);
        $insertOrder = $pdo->prepare(
            'INSERT INTO pos_orders
             (company_id, session_id, order_number, receipt_number, order_date, order_type, original_order_number, customer_code, cashier_user_id,
              cost_center_code, subtotal, discount_total, tax_total, grand_total, paid_total, change_amount, status, invoice_requested, notes)
             VALUES
             (:company_id, :session_id, :order_number, :receipt_number, :order_date, :order_type, :original_order_number, :customer_code, :cashier_user_id,
              :cost_center_code, :subtotal, :discount_total, :tax_total, :grand_total, :paid_total, :change_amount, :status, :invoice_requested, :notes)'
        );
        $status = $orderType === 'refund' ? 'refunded' : 'paid';
        $insertOrder->execute([
            'company_id' => $companyId,
            'session_id' => $sessionId,
            'order_number' => $orderNumber,
            'receipt_number' => $receiptNumber,
            'order_date' => date('Y-m-d H:i:s'),
            'order_type' => $orderType,
            'original_order_number' => app_normalize_whitespace((string) ($input['original_order_number'] ?? '')),
            'customer_code' => $customerCode,
            'cashier_user_id' => app_current_user()['id'] ?? null,
            'cost_center_code' => $costCenterCode,
            'subtotal' => $subtotal,
            'discount_total' => $discountTotal,
            'tax_total' => $taxTotal,
            'grand_total' => $grandTotal,
            'paid_total' => $grandTotal,
            'change_amount' => $changeAmount,
            'status' => $status,
            'invoice_requested' => !empty($input['invoice_requested']) ? 1 : 0,
            'notes' => app_normalize_whitespace((string) ($input['notes'] ?? '')),
        ]);
        $orderId = (int) $pdo->lastInsertId();

        $insertLine = $pdo->prepare(
            'INSERT INTO pos_order_lines
             (company_id, order_id, line_no, sku, product_name, quantity, unit_price, discount_percent, discount_amount,
              tax_rate_percent, tax_name, subtotal, tax_total, total, revenue_account_code, cost_center_code, customer_note)
             VALUES
             (:company_id, :order_id, :line_no, :sku, :product_name, :quantity, :unit_price, :discount_percent, :discount_amount,
              :tax_rate_percent, :tax_name, :subtotal, :tax_total, :total, :revenue_account_code, :cost_center_code, :customer_note)'
        );
        $updateProduct = $pdo->prepare(
            'UPDATE products
             SET qty_on_hand = :qty_on_hand
             WHERE company_id = :company_id
               AND sku = :sku'
        );
        $insertMovement = $pdo->prepare(
            'INSERT INTO inventory_movements
             (company_id, movement_number, movement_date, movement_type, sku, quantity, unit_cost, reference, notes)
             VALUES
             (:company_id, :movement_number, :movement_date, :movement_type, :sku, :quantity, :unit_cost, :reference, :notes)'
        );
        $lineNo = 1;
        foreach ($normalizedLines as $line) {
            $insertLine->execute([
                'company_id' => $companyId,
                'order_id' => $orderId,
                'line_no' => $lineNo++,
                'sku' => $line['sku'],
                'product_name' => $line['product_name'],
                'quantity' => $line['quantity'],
                'unit_price' => $line['unit_price'],
                'discount_percent' => $line['discount_percent'],
                'discount_amount' => $line['discount_amount'],
                'tax_rate_percent' => $line['tax_rate_percent'],
                'tax_name' => $line['tax_name'],
                'subtotal' => $line['subtotal'],
                'tax_total' => $line['tax_total'],
                'total' => $line['total'],
                'revenue_account_code' => $line['revenue_account_code'],
                'cost_center_code' => $line['cost_center_code'],
                'customer_note' => $line['customer_note'],
            ]);
            $updateProduct->execute([
                'qty_on_hand' => $line['stock_after'],
                'company_id' => $companyId,
                'sku' => $line['sku'],
            ]);
            $movementType = app_number($line['quantity'] ?? 0) >= 0 ? 'out' : 'in';
            $insertMovement->execute([
                'company_id' => $companyId,
                'movement_number' => app_next_prefixed_number($companyId, 'inventory_movements', 'movement_number', 'INV-MV'),
                'movement_date' => date('Y-m-d'),
                'movement_type' => $movementType,
                'sku' => $line['sku'],
                'quantity' => abs(app_number($line['quantity'] ?? 0)),
                'unit_cost' => $line['purchase_price'],
                'reference' => $orderNumber,
                'notes' => 'POS stock movement',
            ]);
        }

        $insertPayment = $pdo->prepare(
            'INSERT INTO pos_payments
             (company_id, order_id, line_no, payment_method, payment_account_name, amount, reference)
             VALUES
             (:company_id, :order_id, :line_no, :payment_method, :payment_account_name, :amount, :reference)'
        );
        $paymentNo = 1;
        foreach ($normalizedPayments as $payment) {
            if (abs(app_number($payment['amount'] ?? 0)) < 0.01) {
                continue;
            }
            $insertPayment->execute([
                'company_id' => $companyId,
                'order_id' => $orderId,
                'line_no' => $paymentNo++,
                'payment_method' => $payment['payment_method'],
                'payment_account_name' => $payment['payment_account_name'],
                'amount' => $payment['amount'],
                'reference' => $payment['reference'],
            ]);
        }

        $orderRecord = app_module_find('pos_orders', $orderId, $companyId);
        if (!$orderRecord) {
            throw new RuntimeException('POS order could not be loaded after saving.');
        }
        app_upsert_auto_journal_entry(
            $companyId,
            'pos_orders',
            $orderId,
            substr((string) ($orderRecord['order_date'] ?? date('Y-m-d')), 0, 10),
            (string) ($orderRecord['order_number'] ?? $orderNumber),
            'Auto posting for POS order ' . (string) ($orderRecord['order_number'] ?? $orderNumber),
            app_build_pos_order_journal_lines($companyId, $orderRecord),
            'posted'
        );
        app_pos_sync_session_expected_cash($companyId, $sessionId);

        $pdo->commit();
        return $orderId;
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_pos_close_session(int $companyId, int $sessionId, float $closingCash, string $notes = ''): void
{
    $session = app_pos_session_by_id($companyId, $sessionId);
    if (!$session || strtolower(app_normalize_whitespace((string) ($session['status'] ?? ''))) !== 'open') {
        throw new RuntimeException('Select an open POS session to close.');
    }

    $summary = app_pos_session_summary($companyId, $sessionId);
    $expected = round(app_number($summary['expected_cash'] ?? 0), 2);
    $closingCash = round(max(0, $closingCash), 2);
    $difference = round($closingCash - $expected, 2);
    app_pdo()->prepare(
        "UPDATE pos_sessions
         SET expected_cash_amount = :expected_cash,
             closing_cash_counted = :closing_cash_counted,
             cash_difference = :cash_difference,
             closed_at = :closed_at,
             closed_by_user_id = :closed_by_user_id,
             notes = :notes,
             status = 'closed'
         WHERE company_id = :company_id
           AND id = :id"
    )->execute([
        'expected_cash' => $expected,
        'closing_cash_counted' => $closingCash,
        'cash_difference' => $difference,
        'closed_at' => date('Y-m-d H:i:s'),
        'closed_by_user_id' => app_current_user()['id'] ?? null,
        'notes' => app_normalize_whitespace($notes),
        'company_id' => $companyId,
        'id' => $sessionId,
    ]);

    $closed = app_pos_session_by_id($companyId, $sessionId);
    if ($closed) {
        app_upsert_auto_journal_entry(
            $companyId,
            'pos_sessions',
            $sessionId,
            substr((string) ($closed['closed_at'] ?? date('Y-m-d')), 0, 10),
            (string) ($closed['session_number'] ?? ''),
            'Auto posting for POS session cash difference ' . (string) ($closed['session_number'] ?? ''),
            app_build_pos_session_journal_lines($companyId, $closed),
            'posted'
        );
    }
}

function app_pos_recent_orders(int $companyId, int $limit = 12): array
{
    if ($companyId <= 0) {
        return [];
    }
    $limit = min(50, max(1, $limit));
    $stmt = app_pdo()->prepare(
        'SELECT o.*, s.session_number, r.register_name
         FROM pos_orders o
         INNER JOIN pos_sessions s
                 ON s.company_id = o.company_id
                AND s.id = o.session_id
         INNER JOIN pos_registers r
                 ON r.company_id = s.company_id
                AND r.id = s.register_id
         WHERE o.company_id = :company_id
         ORDER BY o.order_date DESC, o.id DESC
         LIMIT ' . $limit
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_source_entry_number(string $sourceType, int $sourceId): string
{
    $sourceId = max(1, $sourceId);
    $prefix = match ($sourceType) {
        'sales_invoices' => 'SI',
        'sales_returns' => 'SR',
        'student_invoices' => 'STI',
        'student_receipts' => 'STR',
        'student_refunds' => 'STP',
        'purchase_bills' => 'PB',
        'purchase_returns' => 'PR',
        'expenses' => 'EXP',
        'expense_returns' => 'ER',
        'payments' => 'PAY',
        'prepaid_expenses' => 'PRE',
        'prepaid_amortization_runs' => 'PAM',
        'depreciation_runs' => 'DEP',
        'ifrs16_leases' => 'LSE',
        'ifrs16_lease_schedules' => 'LSP',
        'pos_orders' => 'POS',
        'pos_sessions' => 'PSS',
        'pos_cash_movements' => 'PCM',
        default => strtoupper(substr($sourceType, 0, 3)),
    };
    return 'AUTO-' . $prefix . '-' . str_pad((string) $sourceId, 6, '0', STR_PAD_LEFT);
}

function app_journal_entries_index_for_source_ids(int $companyId, string $sourceType, array $sourceIds): array
{
    if ($companyId <= 0 || $sourceType === '') {
        return [];
    }

    $ids = [];
    foreach ($sourceIds as $sourceId) {
        $id = (int) $sourceId;
        if ($id > 0) {
            $ids[$id] = true;
        }
    }
    if (!$ids) {
        return [];
    }

    $idList = array_keys($ids);
    $placeholders = [];
    $params = [
        'company_id' => $companyId,
        'source_type' => $sourceType,
    ];
    foreach ($idList as $idx => $id) {
        $key = 'source_id_' . $idx;
        $placeholders[] = ':' . $key;
        $params[$key] = $id;
    }

    $sql = 'SELECT id, source_id, entry_number, entry_date, status
            FROM journal_entries
            WHERE company_id = :company_id
              AND source_type = :source_type
              AND source_id IN (' . implode(', ', $placeholders) . ')';
    $stmt = app_pdo()->prepare($sql);
    $stmt->execute($params);
    $rows = $stmt->fetchAll() ?: [];

    $index = [];
    foreach ($rows as $row) {
        $sourceId = (int) ($row['source_id'] ?? 0);
        if ($sourceId > 0) {
            $index[$sourceId] = $row;
        }
    }
    return $index;
}

function app_journal_entry_for_source(int $companyId, string $sourceType, int $sourceId): ?array
{
    if ($companyId <= 0 || $sourceType === '' || $sourceId <= 0) {
        return null;
    }
    $index = app_journal_entries_index_for_source_ids($companyId, $sourceType, [$sourceId]);
    return $index[$sourceId] ?? null;
}

function app_source_module_label(string $sourceType): string
{
    $sourceType = app_normalize_whitespace($sourceType);
    if ($sourceType === '') {
        return app_t('Manual');
    }

    $label = match ($sourceType) {
        'sales_invoices' => 'Sales Invoice',
        'sales_returns' => 'Sales Return',
        'student_invoices' => 'Student Invoice',
        'student_receipts' => 'Student Receipt',
        'student_refunds' => 'Student Payment Voucher',
        'purchase_bills' => 'Purchase Bill',
        'purchase_returns' => 'Purchase Return',
        'expenses' => 'Expense',
        'expense_returns' => 'Expense Return',
        'payments' => 'Payment',
        'prepaid_expenses' => 'Prepaid Expense',
        'prepaid_amortization_runs' => 'Prepaid Amortization',
        'depreciation_runs' => 'Automatic Depreciation',
        'ifrs16_leases' => 'IFRS16 Lease',
        'ifrs16_lease_schedules' => 'IFRS16 Lease Schedule',
        'pos_orders' => 'POS Order',
        'pos_sessions' => 'POS Session',
        'pos_cash_movements' => 'POS Cash Movement',
        'excellence_operations' => 'EXcellence Operation',
        'excellence_hospital_expenses' => 'EXcellence Hospital Expense',
        'excellence_admin_expenses' => 'EXcellence Administrative Expense',
        'excellence_salaries' => 'EXcellence Salary',
        'opening' => 'Opening Entry',
        default => app_normalize_whitespace(str_replace('_', ' ', ucwords($sourceType))),
    };
    return app_t($label);
}

function app_source_module_url(string $sourceType, int $sourceId, ?int $companyId = null): ?string
{
    if ($sourceId <= 0) {
        return null;
    }

    $module = match ($sourceType) {
        'sales_invoices', 'sales_returns', 'student_invoices', 'student_receipts', 'student_refunds', 'purchase_bills', 'purchase_returns', 'expenses', 'expense_returns', 'payments', 'prepaid_expenses', 'prepaid_amortization_runs', 'depreciation_runs', 'ifrs16_leases', 'ifrs16_lease_schedules', 'pos_orders', 'pos_sessions', 'pos_cash_movements', 'excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries' => $sourceType,
        default => null,
    };
    if ($module === null) {
        return null;
    }

    $query = [
        'module' => $module,
        'edit' => $sourceId,
    ];
    if ($companyId !== null && $companyId > 0) {
        $query['company_id'] = $companyId;
    }
    return 'module.php?' . http_build_query($query);
}

function app_accounting_reports_catalog(): array
{
    return [
        'ledger' => ['label' => app_t('Account Statement'), 'icon' => 'LG'],
        'contact_statement' => ['label' => app_t('Contact Statement'), 'icon' => 'CT'],
        'journal_book' => ['label' => app_t('Journal Book'), 'icon' => 'JB'],
        'cost_center' => ['label' => app_t('Cost Center Report'), 'icon' => 'CC'],
        'vat_summary' => ['label' => app_t('VAT Summary'), 'icon' => 'VAT'],
        'trial_balance' => ['label' => app_t('Trial Balance'), 'icon' => 'TB'],
        'income_statement' => ['label' => app_t('Income Statement'), 'icon' => 'IS'],
        'balance_sheet' => ['label' => app_t('Statement of Financial Position'), 'icon' => 'FP'],
        'expected_credit_loss' => ['label' => app_t('Expected Credit Loss'), 'icon' => 'ECL'],
        'cash_flow' => ['label' => app_t('Cash Flow'), 'icon' => 'CF'],
    ];
}

function app_accounting_vat_summary(int $companyId, string $fromDate, string $toDate): array
{
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }
    if ($companyId <= 0) {
        return [];
    }

    $pdo = app_pdo();
    $sumTable = static function (string $table, string $dateColumn, string $taxableExpr, string $taxExpr, string $rangeFrom, string $rangeTo, string $baseColumn = 'subtotal') use ($pdo, $companyId): array {
        $baseColumn = app_identifier($baseColumn) ? $baseColumn : 'subtotal';
        $stmt = $pdo->prepare(
            'SELECT
                COALESCE(SUM(CASE WHEN ' . $taxableExpr . ' THEN `' . $baseColumn . '` ELSE 0 END), 0) AS taxable_base,
                COALESCE(SUM(CASE WHEN NOT (' . $taxableExpr . ') THEN `' . $baseColumn . '` ELSE 0 END), 0) AS exempt_base,
                COALESCE(SUM(CASE WHEN ' . $taxExpr . ' THEN tax_total ELSE 0 END), 0) AS tax_total
             FROM `' . $table . '`
             WHERE company_id = :company_id
               AND `' . $dateColumn . '` >= :from_date
               AND `' . $dateColumn . '` <= :to_date
               AND status NOT IN (\'draft\', \'cancelled\')'
        );
        $stmt->execute(['company_id' => $companyId, 'from_date' => $rangeFrom, 'to_date' => $rangeTo]);
        $row = $stmt->fetch() ?: [];
        return [
            'taxable_base' => max(0, app_number($row['taxable_base'] ?? 0)),
            'exempt_base' => max(0, app_number($row['exempt_base'] ?? 0)),
            'tax_total' => max(0, app_number($row['tax_total'] ?? 0)),
        ];
    };

    $buildSummary = static function (string $rangeFrom, string $rangeTo) use ($sumTable, $pdo, $companyId): array {
        $sales = $sumTable('sales_invoices', 'invoice_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);
        $salesReturns = $sumTable('sales_returns', 'return_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);
        $posStmt = $pdo->prepare(
            "SELECT
                COALESCE(SUM(CASE WHEN tax_total <> 0 THEN subtotal ELSE 0 END), 0) AS taxable_base,
                COALESCE(SUM(CASE WHEN tax_total = 0 THEN subtotal ELSE 0 END), 0) AS exempt_base,
                COALESCE(SUM(tax_total), 0) AS tax_total
             FROM pos_orders
             WHERE company_id = :company_id
               AND DATE(order_date) >= :from_date
               AND DATE(order_date) <= :to_date
               AND status <> 'cancelled'"
        );
        $posStmt->execute(['company_id' => $companyId, 'from_date' => $rangeFrom, 'to_date' => $rangeTo]);
        $posRow = $posStmt->fetch() ?: [];
        $posSales = [
            'taxable_base' => app_number($posRow['taxable_base'] ?? 0),
            'exempt_base' => app_number($posRow['exempt_base'] ?? 0),
            'tax_total' => app_number($posRow['tax_total'] ?? 0),
        ];
        $purchaseBills = $sumTable('purchase_bills', 'bill_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);
        $expenses = $sumTable('expenses', 'expense_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);
        $prepaidExpenses = $sumTable('prepaid_expenses', 'prepaid_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo, 'total_amount');
        $purchaseReturns = $sumTable('purchase_returns', 'return_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);
        $expenseReturns = $sumTable('expense_returns', 'return_date', 'is_taxable = 1 AND tax_rate_percent > 0', 'is_taxable = 1', $rangeFrom, $rangeTo);

        $purchases = [
            'taxable_base' => $purchaseBills['taxable_base'] + $expenses['taxable_base'] + $prepaidExpenses['taxable_base'],
            'exempt_base' => $purchaseBills['exempt_base'] + $expenses['exempt_base'] + $prepaidExpenses['exempt_base'],
            'tax_total' => $purchaseBills['tax_total'] + $expenses['tax_total'] + $prepaidExpenses['tax_total'],
        ];
        $purchaseReturnTotals = [
            'taxable_base' => $purchaseReturns['taxable_base'] + $expenseReturns['taxable_base'],
            'exempt_base' => $purchaseReturns['exempt_base'] + $expenseReturns['exempt_base'],
            'tax_total' => $purchaseReturns['tax_total'] + $expenseReturns['tax_total'],
        ];

        $sales['taxable_base'] += $posSales['taxable_base'];
        $sales['exempt_base'] += $posSales['exempt_base'];
        $sales['tax_total'] += $posSales['tax_total'];

        $salesTaxNet = $sales['tax_total'] - $salesReturns['tax_total'];
        $purchaseTaxNet = $purchases['tax_total'] - $purchaseReturnTotals['tax_total'];

        return [
            'sales_taxable' => ['base' => $sales['taxable_base'], 'return_base' => $salesReturns['taxable_base'], 'tax' => $salesTaxNet],
            'sales_exempt' => ['base' => $sales['exempt_base'], 'return_base' => $salesReturns['exempt_base'], 'tax' => 0.0],
            'sales_total' => ['base' => $sales['taxable_base'] + $sales['exempt_base'], 'return_base' => $salesReturns['taxable_base'] + $salesReturns['exempt_base'], 'tax' => $salesTaxNet],
            'purchase_taxable' => ['base' => $purchases['taxable_base'], 'return_base' => $purchaseReturnTotals['taxable_base'], 'tax' => $purchaseTaxNet],
            'purchase_total' => ['base' => $purchases['taxable_base'] + $purchases['exempt_base'], 'return_base' => $purchaseReturnTotals['taxable_base'] + $purchaseReturnTotals['exempt_base'], 'tax' => $purchaseTaxNet],
            'summary' => ['sales_tax' => $salesTaxNet, 'purchase_tax' => $purchaseTaxNet, 'net_vat' => $salesTaxNet - $purchaseTaxNet],
        ];
    };

    $taxAccountsCarriedForward = static function (string $beforeDate) use ($pdo, $companyId): float {
        $beforeDate = app_sanitize_date($beforeDate, date('Y-m-d'));
        $accountStmt = $pdo->prepare(
            'SELECT DISTINCT tax_account_code
             FROM tax_rates
             WHERE company_id = :company_id
               AND is_active = 1
               AND applies_to IN (\'sales\', \'purchase\', \'both\')
               AND tax_account_code <> \'\''
        );
        $accountStmt->execute(['company_id' => $companyId]);
        $accounts = [];
        foreach ($accountStmt->fetchAll(PDO::FETCH_COLUMN) ?: [] as $accountCode) {
            $accountCode = app_normalize_whitespace((string) $accountCode);
            if ($accountCode !== '') {
                $accounts[$accountCode] = true;
            }
        }
        if (!$accounts) {
            return 0.0;
        }

        $params = [
            'company_id_entries' => $companyId,
            'company_id_lines' => $companyId,
            'status' => 'posted',
            'before_date' => $beforeDate,
        ];
        $placeholders = [];
        foreach (array_keys($accounts) as $index => $accountCode) {
            $key = 'account_code_' . $index;
            $placeholders[] = ':' . $key;
            $params[$key] = $accountCode;
        }

        $stmt = $pdo->prepare(
            'SELECT COALESCE(SUM(l.credit - l.debit), 0)
             FROM journal_entry_lines l
             INNER JOIN journal_entries j ON j.id = l.journal_entry_id
             WHERE j.company_id = :company_id_entries
               AND l.company_id = :company_id_lines
               AND j.status = :status
               AND j.entry_date < :before_date
               AND l.account_code IN (' . implode(', ', $placeholders) . ')'
        );
        $stmt->execute($params);
        return round(app_number($stmt->fetchColumn() ?: 0), 2);
    };

    $summary = $buildSummary($fromDate, $toDate);
    $carriedForward = $taxAccountsCarriedForward($fromDate);
    $currentNet = app_number($summary['summary']['net_vat'] ?? 0);
    $summary['summary']['current_period_vat'] = $currentNet;
    $summary['summary']['carried_forward_vat'] = $carriedForward;
    $summary['summary']['net_due_or_recoverable'] = $currentNet + $carriedForward;

    return $summary;
}

function app_upsert_auto_journal_entry(
    int $companyId,
    string $sourceType,
    int $sourceId,
    string $entryDate,
    string $reference,
    string $description,
    array $lines,
    string $entryStatus = 'posted'
): void {
    if ($companyId <= 0 || $sourceId <= 0) {
        throw new RuntimeException('Invalid source posting context.');
    }
    if (!$lines) {
        app_delete_auto_journal_entry($companyId, $sourceType, $sourceId);
        return;
    }

    $totalDebit = 0.0;
    $totalCredit = 0.0;
    $accountTypeCache = [];
    foreach ($lines as $line) {
        $accountCode = app_normalize_whitespace((string) ($line['account_code'] ?? ''));
        app_assert_leaf_account($companyId, $accountCode, null, 'Journal line account');
        if (!isset($accountTypeCache[$accountCode])) {
            $accountTypeCache[$accountCode] = app_account_type_by_code($companyId, $accountCode) ?? '';
        }
        $accountType = strtolower((string) $accountTypeCache[$accountCode]);
        $costCenterCode = app_normalize_whitespace((string) ($line['cost_center_code'] ?? ''));
        if (in_array($accountType, ['revenue', 'expense'], true) && $costCenterCode === '') {
            throw new RuntimeException('Cost center is required for revenue/expense journal lines.');
        }
        if ($costCenterCode !== '' && !app_cost_center_exists($companyId, $costCenterCode, false)) {
            throw new RuntimeException('Invalid cost center in journal line: ' . $costCenterCode);
        }
        $debit = max(0, app_number($line['debit'] ?? 0));
        $credit = max(0, app_number($line['credit'] ?? 0));
        $totalDebit += $debit;
        $totalCredit += $credit;
    }
    if (round($totalDebit, 2) !== round($totalCredit, 2)) {
        throw new RuntimeException('Auto-generated journal entry is not balanced.');
    }

    $pdo = app_pdo();
    $entryDate = app_sanitize_date($entryDate, date('Y-m-d'));
    $reference = app_normalize_whitespace($reference);
    $description = app_normalize_whitespace($description);
    $entryStatus = strtolower(app_normalize_whitespace($entryStatus));
    if (!in_array($entryStatus, ['draft', 'posted'], true)) {
        $entryStatus = 'posted';
    }
    $entryNumber = app_source_entry_number($sourceType, $sourceId);

    $findStmt = $pdo->prepare(
        'SELECT id
         FROM journal_entries
         WHERE company_id = :company_id
           AND source_type = :source_type
           AND source_id = :source_id
         LIMIT 1'
    );
    $findStmt->execute([
        'company_id' => $companyId,
        'source_type' => $sourceType,
        'source_id' => $sourceId,
    ]);
    $entryId = (int) ($findStmt->fetchColumn() ?: 0);

    if ($entryId > 0) {
        $updateStmt = $pdo->prepare(
            'UPDATE journal_entries
             SET entry_number = :entry_number,
                 entry_date = :entry_date,
                 reference = :reference,
                 description = :description,
                 total_debit = :total_debit,
                 total_credit = :total_credit,
                 status = :status,
                 is_system_generated = 1
             WHERE id = :id AND company_id = :company_id'
        );
        $updateStmt->execute([
            'entry_number' => $entryNumber,
            'entry_date' => $entryDate,
            'reference' => $reference,
            'description' => $description,
            'total_debit' => round($totalDebit, 2),
            'total_credit' => round($totalCredit, 2),
            'status' => $entryStatus,
            'id' => $entryId,
            'company_id' => $companyId,
        ]);
    } else {
        $insertStmt = $pdo->prepare(
            'INSERT INTO journal_entries
             (company_id, entry_number, entry_date, reference, description, total_debit, total_credit, status, source_type, source_id, is_system_generated, created_by_user_id)
             VALUES
             (:company_id, :entry_number, :entry_date, :reference, :description, :total_debit, :total_credit, :status, :source_type, :source_id, 1, :created_by_user_id)'
        );
        $insertStmt->execute([
            'company_id' => $companyId,
            'entry_number' => $entryNumber,
            'entry_date' => $entryDate,
            'reference' => $reference,
            'description' => $description,
            'total_debit' => round($totalDebit, 2),
            'total_credit' => round($totalCredit, 2),
            'status' => $entryStatus,
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'created_by_user_id' => app_current_user()['id'] ?? null,
        ]);
        $entryId = (int) $pdo->lastInsertId();
    }

    $pdo->prepare('DELETE FROM journal_entry_lines WHERE journal_entry_id = :journal_entry_id')
        ->execute(['journal_entry_id' => $entryId]);

    $insertLineStmt = $pdo->prepare(
        'INSERT INTO journal_entry_lines
         (company_id, journal_entry_id, line_no, account_code, cost_center_code, debit, credit, description, source_type, source_id)
         VALUES
         (:company_id, :journal_entry_id, :line_no, :account_code, :cost_center_code, :debit, :credit, :description, :source_type, :source_id)'
    );

    $lineNo = 1;
    foreach ($lines as $line) {
        $insertLineStmt->execute([
            'company_id' => $companyId,
            'journal_entry_id' => $entryId,
            'line_no' => $lineNo++,
            'account_code' => app_normalize_whitespace((string) ($line['account_code'] ?? '')),
            'cost_center_code' => app_normalize_whitespace((string) ($line['cost_center_code'] ?? '')),
            'debit' => round(max(0, app_number($line['debit'] ?? 0)), 2),
            'credit' => round(max(0, app_number($line['credit'] ?? 0)), 2),
            'description' => app_normalize_whitespace((string) ($line['description'] ?? '')),
            'source_type' => $sourceType,
            'source_id' => $sourceId,
        ]);
    }
}

function app_parse_manual_journal_entry_lines(array $input): array
{
    $accounts = array_values((array) ($input['line_account_code'] ?? []));
    $descriptions = array_values((array) ($input['line_description'] ?? []));
    $costCenters = array_values((array) ($input['line_cost_center_code'] ?? []));
    $debits = array_values((array) ($input['line_debit'] ?? []));
    $credits = array_values((array) ($input['line_credit'] ?? []));
    $lineCount = max(count($accounts), count($descriptions), count($costCenters), count($debits), count($credits));
    $lines = [];

    for ($index = 0; $index < $lineCount; $index++) {
        $accountCode = app_normalize_whitespace((string) ($accounts[$index] ?? ''));
        $description = app_normalize_whitespace((string) ($descriptions[$index] ?? ''));
        $costCenterCode = app_normalize_whitespace((string) ($costCenters[$index] ?? ''));
        $debit = round(max(0, app_number($debits[$index] ?? 0)), 2);
        $credit = round(max(0, app_number($credits[$index] ?? 0)), 2);

        if ($accountCode === '' && $description === '' && $costCenterCode === '' && $debit <= 0 && $credit <= 0) {
            continue;
        }

        $lines[] = [
            'account_code' => $accountCode,
            'cost_center_code' => $costCenterCode,
            'description' => $description,
            'debit' => $debit,
            'credit' => $credit,
        ];
    }

    return $lines;
}

function app_validate_journal_entry_lines(int $companyId, array $lines): array
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for journal entries.');
    }
    if (count($lines) < 2) {
        throw new RuntimeException('Journal entry must contain at least two journal lines.');
    }

    $totalDebit = 0.0;
    $totalCredit = 0.0;
    $validLines = [];
    $accountTypeCache = [];

    foreach ($lines as $index => $line) {
        $displayLineNo = $index + 1;
        $accountCode = app_normalize_whitespace((string) ($line['account_code'] ?? ''));
        if ($accountCode === '') {
            throw new RuntimeException('Journal line ' . $displayLineNo . ': account is required.');
        }
        app_assert_leaf_account($companyId, $accountCode, null, 'Journal line account');

        $debit = round(max(0, app_number($line['debit'] ?? 0)), 2);
        $credit = round(max(0, app_number($line['credit'] ?? 0)), 2);
        if ($debit <= 0 && $credit <= 0) {
            throw new RuntimeException('Journal line ' . $displayLineNo . ': enter a debit or credit amount.');
        }
        if ($debit > 0 && $credit > 0) {
            throw new RuntimeException('Journal line ' . $displayLineNo . ': debit and credit cannot both have values.');
        }

        if (!isset($accountTypeCache[$accountCode])) {
            $accountTypeCache[$accountCode] = app_account_type_by_code($companyId, $accountCode) ?? '';
        }
        $accountType = strtolower((string) $accountTypeCache[$accountCode]);
        $costCenterCode = app_normalize_whitespace((string) ($line['cost_center_code'] ?? ''));
        if (in_array($accountType, ['revenue', 'expense'], true) && $costCenterCode === '') {
            throw new RuntimeException('Journal line ' . $displayLineNo . ': cost center is required for revenue and expense accounts.');
        }
        if ($costCenterCode !== '' && !app_cost_center_exists($companyId, $costCenterCode)) {
            throw new RuntimeException('Journal line ' . $displayLineNo . ': invalid cost center "' . $costCenterCode . '".');
        }

        $totalDebit += $debit;
        $totalCredit += $credit;
        $validLines[] = [
            'line_no' => count($validLines) + 1,
            'account_code' => $accountCode,
            'cost_center_code' => $costCenterCode,
            'description' => app_normalize_whitespace((string) ($line['description'] ?? '')),
            'debit' => $debit,
            'credit' => $credit,
        ];
    }

    $totalDebit = round($totalDebit, 2);
    $totalCredit = round($totalCredit, 2);
    if ($totalDebit <= 0 || abs($totalDebit - $totalCredit) >= 0.01) {
        throw new RuntimeException('Journal entry must be balanced before saving or posting.');
    }

    return [
        'lines' => $validLines,
        'total_debit' => $totalDebit,
        'total_credit' => $totalCredit,
    ];
}

function app_save_manual_journal_entry(int $companyId, array $input, ?int $id = null): int
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for journal entries.');
    }

    $existing = $id !== null && $id > 0 ? app_module_find('journal_entries', (int) $id, $companyId) : null;
    if ($id !== null && $id > 0 && !$existing) {
        throw new RuntimeException('Journal entry was not found.');
    }
    if ($existing && (int) ($existing['is_system_generated'] ?? 0) === 1) {
        throw new RuntimeException('System-generated journal entries are controlled by their source document.');
    }
    if ($existing) {
        $existingStatus = strtolower(app_normalize_whitespace((string) ($existing['status'] ?? 'draft')));
        if ($existingStatus !== 'draft') {
            throw new RuntimeException('Reset the journal entry to Draft before editing its lines.');
        }
    }

    $validated = app_validate_journal_entry_lines($companyId, app_parse_manual_journal_entry_lines($input));
    $description = app_normalize_whitespace((string) ($input['description'] ?? ''));
    $status = strtolower(app_normalize_whitespace((string) ($input['status'] ?? ($existing['status'] ?? 'draft'))));
    if (!in_array($status, ['draft', 'posted', 'reversed'], true)) {
        $status = 'draft';
    }

    $payload = [
        'entry_number' => app_normalize_whitespace((string) ($input['entry_number'] ?? '')),
        'entry_date' => app_sanitize_date((string) ($input['entry_date'] ?? ''), date('Y-m-d')),
        'reference' => app_normalize_whitespace((string) ($input['reference'] ?? '')),
        'description' => $description,
        'total_debit' => (float) $validated['total_debit'],
        'total_credit' => (float) $validated['total_credit'],
        'status' => $status,
    ];

    $pdo = app_pdo();
    $startedTransaction = false;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        $savedId = app_module_save('journal_entries', $payload, $companyId, $id !== null && $id > 0 ? (int) $id : null);

        $pdo->prepare(
            'DELETE FROM journal_entry_lines
             WHERE company_id = :company_id
               AND journal_entry_id = :journal_entry_id'
        )->execute([
            'company_id' => $companyId,
            'journal_entry_id' => $savedId,
        ]);

        $insertLineStmt = $pdo->prepare(
            'INSERT INTO journal_entry_lines
             (company_id, journal_entry_id, line_no, account_code, cost_center_code, debit, credit, description, source_type, source_id)
             VALUES
             (:company_id, :journal_entry_id, :line_no, :account_code, :cost_center_code, :debit, :credit, :description, :source_type, :source_id)'
        );
        foreach ((array) $validated['lines'] as $line) {
            $lineDescription = app_normalize_whitespace((string) ($line['description'] ?? ''));
            if ($lineDescription === '') {
                $lineDescription = $description;
            }
            $insertLineStmt->execute([
                'company_id' => $companyId,
                'journal_entry_id' => $savedId,
                'line_no' => (int) ($line['line_no'] ?? 0),
                'account_code' => (string) ($line['account_code'] ?? ''),
                'cost_center_code' => (string) ($line['cost_center_code'] ?? ''),
                'debit' => (float) ($line['debit'] ?? 0),
                'credit' => (float) ($line['credit'] ?? 0),
                'description' => $lineDescription,
                'source_type' => '',
                'source_id' => null,
            ]);
        }

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }

        return (int) $savedId;
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_delete_auto_journal_entry(int $companyId, string $sourceType, int $sourceId): void
{
    if ($companyId <= 0 || $sourceId <= 0) {
        return;
    }
    $pdo = app_pdo();
    $findStmt = $pdo->prepare(
        'SELECT id
         FROM journal_entries
         WHERE company_id = :company_id
           AND source_type = :source_type
           AND source_id = :source_id'
    );
    $findStmt->execute([
        'company_id' => $companyId,
        'source_type' => $sourceType,
        'source_id' => $sourceId,
    ]);
    $entryIds = array_map('intval', $findStmt->fetchAll(PDO::FETCH_COLUMN) ?: []);
    if ($entryIds) {
        $placeholders = [];
        $params = ['company_id' => $companyId];
        foreach ($entryIds as $index => $entryId) {
            $key = 'entry_id_' . $index;
            $placeholders[] = ':' . $key;
            $params[$key] = $entryId;
        }
        $pdo->prepare(
            'DELETE FROM journal_entry_lines
             WHERE company_id = :company_id
               AND journal_entry_id IN (' . implode(', ', $placeholders) . ')'
        )->execute($params);
    }

    $stmt = $pdo->prepare(
        'DELETE FROM journal_entries
         WHERE company_id = :company_id
           AND source_type = :source_type
           AND source_id = :source_id'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'source_type' => $sourceType,
        'source_id' => $sourceId,
    ]);
}

function app_sync_student_invoice_payment_status(int $companyId, int $studentInvoiceId): void
{
    if ($companyId <= 0 || $studentInvoiceId <= 0) {
        return;
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT id, grand_total, paid_amount, inline_paid_amount, status
         FROM student_invoices
         WHERE company_id = :company_id AND id = :id
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'id' => $studentInvoiceId]);
    $invoice = $stmt->fetch();
    if (!$invoice) {
        return;
    }

    $sumStmt = $pdo->prepare(
        'SELECT COALESCE(SUM(amount), 0)
         FROM student_receipts
         WHERE company_id = :company_id
           AND student_invoice_id = :student_invoice_id'
    );
    $sumStmt->execute([
        'company_id' => $companyId,
        'student_invoice_id' => $studentInvoiceId,
    ]);

    $paidFromReceipts = max(0, app_number($sumStmt->fetchColumn()));
    $inlinePaid = max(0, app_number($invoice['inline_paid_amount'] ?? 0));
    $paidAmount = $inlinePaid + $paidFromReceipts;
    $grandTotal = max(0, app_number($invoice['grand_total'] ?? 0));
    if ($paidAmount > $grandTotal) {
        $paidAmount = $grandTotal;
    }
    $status = app_document_payment_status('student_invoices', $grandTotal, $paidAmount, (string) ($invoice['status'] ?? 'issued'));

    $pdo->prepare(
        'UPDATE student_invoices
         SET paid_amount = :paid_amount,
             status = :status
         WHERE id = :id
           AND company_id = :company_id'
    )->execute([
        'paid_amount' => round($paidAmount, 2),
        'status' => $status,
        'id' => $studentInvoiceId,
        'company_id' => $companyId,
    ]);
}

function app_sync_student_receipt_related_invoice(int $companyId, array $receipt): void
{
    $invoiceId = app_integer($receipt['student_invoice_id'] ?? 0);
    if ($invoiceId > 0) {
        app_sync_student_invoice_payment_status($companyId, $invoiceId);
    }
}

function app_sync_document_payment_status_by_reference(int $companyId, string $reference): void
{
    $reference = app_normalize_whitespace($reference);
    if ($companyId <= 0 || $reference === '') {
        return;
    }

    $pdo = app_pdo();

    $salesStmt = $pdo->prepare(
        'SELECT id, grand_total, paid_amount, inline_paid_amount, status
         FROM sales_invoices
         WHERE company_id = :company_id AND invoice_number = :reference
         LIMIT 1'
    );
    $salesStmt->execute(['company_id' => $companyId, 'reference' => $reference]);
    $sales = $salesStmt->fetch();
    if ($sales) {
        $sumStmt = $pdo->prepare(
            'SELECT COALESCE(SUM(amount), 0)
             FROM payments
             WHERE company_id = :company_id
               AND direction = :direction
               AND (reference = :reference OR sales_invoice_id = :sales_invoice_id)'
        );
        $sumStmt->execute([
            'company_id' => $companyId,
            'reference' => $reference,
            'sales_invoice_id' => (int) $sales['id'],
            'direction' => 'in',
        ]);
        $paidFromPayments = max(0, app_number($sumStmt->fetchColumn()));
        $inlinePaid = max(0, app_number($sales['inline_paid_amount'] ?? 0));
        $paidAmount = $inlinePaid + $paidFromPayments;
        $grandTotal = max(0, app_number($sales['grand_total'] ?? 0));
        if ($paidAmount > $grandTotal) {
            $paidAmount = $grandTotal;
        }
        $status = app_document_payment_status('sales_invoices', $grandTotal, $paidAmount, (string) ($sales['status'] ?? 'issued'));

        $updateSales = $pdo->prepare(
            'UPDATE sales_invoices
             SET paid_amount = :paid_amount, status = :status
             WHERE id = :id AND company_id = :company_id'
        );
        $updateSales->execute([
            'paid_amount' => round($paidAmount, 2),
            'status' => $status,
            'id' => (int) $sales['id'],
            'company_id' => $companyId,
        ]);
    }

    $purchaseStmt = $pdo->prepare(
        'SELECT id, grand_total, withholding_tax_total, net_payable, paid_amount, inline_paid_amount, status
         FROM purchase_bills
         WHERE company_id = :company_id AND bill_number = :reference
         LIMIT 1'
    );
    $purchaseStmt->execute(['company_id' => $companyId, 'reference' => $reference]);
    $purchase = $purchaseStmt->fetch();
    if ($purchase) {
        $sumStmt = $pdo->prepare(
            'SELECT COALESCE(SUM(amount), 0)
             FROM payments
             WHERE company_id = :company_id
               AND direction = :direction
               AND (reference = :reference OR purchase_bill_id = :purchase_bill_id)'
        );
        $sumStmt->execute([
            'company_id' => $companyId,
            'reference' => $reference,
            'purchase_bill_id' => (int) $purchase['id'],
            'direction' => 'out',
        ]);
        $paidFromPayments = max(0, app_number($sumStmt->fetchColumn()));
        $inlinePaid = max(0, app_number($purchase['inline_paid_amount'] ?? 0));
        $paidAmount = $inlinePaid + $paidFromPayments;
        $grandTotal = max(0, app_number($purchase['grand_total'] ?? 0));
        $withholdingTaxTotal = max(0, app_number($purchase['withholding_tax_total'] ?? 0));
        $netPayable = max(0, app_number($purchase['net_payable'] ?? 0));
        if ($netPayable <= 0 && $grandTotal > 0) {
            $netPayable = max(0, $grandTotal - $withholdingTaxTotal);
        }
        $paymentBasisTotal = $netPayable > 0 ? $netPayable : $grandTotal;
        if ($paidAmount > $paymentBasisTotal) {
            $paidAmount = $paymentBasisTotal;
        }
        $status = app_document_payment_status('purchase_bills', $paymentBasisTotal, $paidAmount, (string) ($purchase['status'] ?? 'approved'));

        $updatePurchase = $pdo->prepare(
            'UPDATE purchase_bills
             SET paid_amount = :paid_amount, status = :status
             WHERE id = :id AND company_id = :company_id'
        );
        $updatePurchase->execute([
            'paid_amount' => round($paidAmount, 2),
            'status' => $status,
            'id' => (int) $purchase['id'],
            'company_id' => $companyId,
        ]);
    }

    $expenseStmt = $pdo->prepare(
        'SELECT id, grand_total, paid_amount, inline_paid_amount, status
         FROM expenses
         WHERE company_id = :company_id AND expense_number = :reference
         LIMIT 1'
    );
    $expenseStmt->execute(['company_id' => $companyId, 'reference' => $reference]);
    $expense = $expenseStmt->fetch();
    if ($expense) {
        $sumStmt = $pdo->prepare(
            'SELECT COALESCE(SUM(amount), 0)
             FROM payments
             WHERE company_id = :company_id
               AND direction = :direction
               AND (reference = :reference OR expense_id = :expense_id)'
        );
        $sumStmt->execute([
            'company_id' => $companyId,
            'reference' => $reference,
            'expense_id' => (int) $expense['id'],
            'direction' => 'out',
        ]);
        $paidFromPayments = max(0, app_number($sumStmt->fetchColumn()));
        $inlinePaid = max(0, app_number($expense['inline_paid_amount'] ?? 0));
        $paidAmount = $inlinePaid + $paidFromPayments;
        $grandTotal = max(0, app_number($expense['grand_total'] ?? 0));
        if ($paidAmount > $grandTotal) {
            $paidAmount = $grandTotal;
        }
        $status = app_document_payment_status('expenses', $grandTotal, $paidAmount, (string) ($expense['status'] ?? 'approved'));

        $updateExpense = $pdo->prepare(
            'UPDATE expenses
             SET paid_amount = :paid_amount, status = :status
             WHERE id = :id AND company_id = :company_id'
        );
        $updateExpense->execute([
            'paid_amount' => round($paidAmount, 2),
            'status' => $status,
            'id' => (int) $expense['id'],
            'company_id' => $companyId,
        ]);
    }
}

function app_sync_payment_related_documents(int $companyId, array $payment): void
{
    if ($companyId <= 0) {
        return;
    }

    $reference = app_normalize_whitespace((string) ($payment['reference'] ?? ''));
    if ($reference !== '') {
        app_sync_document_payment_status_by_reference($companyId, $reference);
    }

    $salesInvoiceId = app_integer($payment['sales_invoice_id'] ?? 0);
    if ($salesInvoiceId > 0) {
        $invoice = app_sales_invoice_by_id($companyId, $salesInvoiceId);
        if ($invoice) {
            $invoiceNumber = app_normalize_whitespace((string) ($invoice['invoice_number'] ?? ''));
            if ($invoiceNumber !== '' && $invoiceNumber !== $reference) {
                app_sync_document_payment_status_by_reference($companyId, $invoiceNumber);
            }
        }
    }

    $billId = app_integer($payment['purchase_bill_id'] ?? 0);
    if ($billId > 0) {
        $bill = app_purchase_bill_by_id($companyId, $billId);
        if ($bill) {
            $billNumber = app_normalize_whitespace((string) ($bill['bill_number'] ?? ''));
            if ($billNumber !== '' && $billNumber !== $reference) {
                app_sync_document_payment_status_by_reference($companyId, $billNumber);
            }
        }
    }

    $expenseId = app_integer($payment['expense_id'] ?? 0);
    if ($expenseId > 0) {
        $expense = app_expense_by_id($companyId, $expenseId);
        if ($expense) {
            $expenseNumber = app_normalize_whitespace((string) ($expense['expense_number'] ?? ''));
            if ($expenseNumber !== '' && $expenseNumber !== $reference) {
                app_sync_document_payment_status_by_reference($companyId, $expenseNumber);
            }
        }
    }
}

function app_sync_auto_posting_for_record(string $slug, array $record, int $companyId): void
{
    if (!app_module_requires_auto_posting($slug) || $companyId <= 0) {
        return;
    }

    $sourceId = (int) ($record['id'] ?? 0);
    if ($sourceId <= 0) {
        return;
    }

    if (!app_module_record_should_post($slug, $record)) {
        app_delete_auto_journal_entry($companyId, $slug, $sourceId);
        if ($slug === 'payments') {
            app_sync_payment_related_documents($companyId, $record);
        } elseif ($slug === 'sales_invoices') {
            app_sync_document_payment_status_by_reference($companyId, (string) ($record['invoice_number'] ?? ''));
        } elseif ($slug === 'student_invoices') {
            app_sync_student_invoice_payment_status($companyId, (int) ($record['id'] ?? 0));
        } elseif ($slug === 'student_receipts') {
            app_sync_student_receipt_related_invoice($companyId, $record);
        } elseif ($slug === 'purchase_bills') {
            app_sync_document_payment_status_by_reference($companyId, (string) ($record['bill_number'] ?? ''));
        } elseif ($slug === 'expenses') {
            app_sync_document_payment_status_by_reference($companyId, (string) ($record['expense_number'] ?? ''));
        }
        return;
    }

    $entryDate = date('Y-m-d');
    $reference = '';
    $description = '';
    $lines = [];
    $entryStatus = 'posted';

    if ($slug === 'sales_invoices') {
        $entryDate = (string) ($record['invoice_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['invoice_number'] ?? ''));
        $description = 'Auto posting for Sales Invoice ' . $reference;
        $lines = app_build_sales_invoice_journal_lines($companyId, $record);
        app_sync_document_payment_status_by_reference($companyId, $reference);
    } elseif ($slug === 'student_invoices') {
        $entryDate = (string) ($record['invoice_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['student_invoice_number'] ?? ''));
        $description = 'Auto posting for Student Invoice ' . $reference;
        $lines = app_build_student_invoice_journal_lines($companyId, $record);
        app_sync_student_invoice_payment_status($companyId, (int) ($record['id'] ?? 0));
    } elseif ($slug === 'student_receipts') {
        $entryDate = (string) ($record['receipt_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['receipt_number'] ?? ''));
        $description = 'Auto posting for Student Receipt ' . $reference;
        $lines = app_build_student_receipt_journal_lines($companyId, $record);
        app_sync_student_receipt_related_invoice($companyId, $record);
    } elseif ($slug === 'student_refunds') {
        $entryDate = (string) ($record['voucher_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['voucher_number'] ?? ''));
        $description = 'Auto posting for Student Payment Voucher ' . $reference;
        $lines = app_build_student_refund_journal_lines($companyId, $record);
    } elseif ($slug === 'purchase_bills') {
        $entryDate = (string) ($record['bill_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['bill_number'] ?? ''));
        $description = 'Auto posting for Purchase Bill ' . $reference;
        $lines = app_build_purchase_bill_journal_lines($companyId, $record);
        app_sync_document_payment_status_by_reference($companyId, $reference);
    } elseif ($slug === 'sales_returns') {
        $entryDate = (string) ($record['return_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['return_number'] ?? ''));
        $description = 'Auto posting for Sales Return ' . $reference;
        $lines = app_build_sales_return_journal_lines($companyId, $record);
    } elseif ($slug === 'purchase_returns') {
        $entryDate = (string) ($record['return_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['return_number'] ?? ''));
        $description = 'Auto posting for Purchase Return ' . $reference;
        $lines = app_build_purchase_return_journal_lines($companyId, $record);
    } elseif ($slug === 'expense_returns') {
        $entryDate = (string) ($record['return_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['return_number'] ?? ''));
        $description = 'Auto posting for Expense Return ' . $reference;
        $lines = app_build_expense_return_journal_lines($companyId, $record);
    } elseif ($slug === 'expenses') {
        $entryDate = (string) ($record['expense_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['expense_number'] ?? ''));
        $description = 'Auto posting for Expense ' . $reference;
        $lines = app_build_expense_journal_lines($companyId, $record);
        app_sync_document_payment_status_by_reference($companyId, $reference);
    } elseif ($slug === 'payments') {
        $entryDate = (string) ($record['payment_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['payment_number'] ?? ''));
        $description = 'Auto posting for Payment ' . $reference;
        $lines = app_build_payment_journal_lines($companyId, $record);
        app_sync_payment_related_documents($companyId, $record);
    } elseif ($slug === 'pos_orders') {
        $entryDate = substr((string) ($record['order_date'] ?? date('Y-m-d')), 0, 10);
        $reference = app_normalize_whitespace((string) ($record['order_number'] ?? ''));
        $description = 'Auto posting for POS Order ' . $reference;
        $lines = app_build_pos_order_journal_lines($companyId, $record);
    } elseif ($slug === 'pos_cash_movements') {
        $entryDate = substr((string) ($record['movement_date'] ?? date('Y-m-d')), 0, 10);
        $reference = app_normalize_whitespace((string) ($record['movement_number'] ?? ''));
        $description = 'Auto posting for POS Cash Movement ' . $reference;
        $lines = app_build_pos_cash_movement_journal_lines($companyId, $record);
    } elseif ($slug === 'pos_sessions') {
        $entryDate = substr((string) ($record['closed_at'] ?? date('Y-m-d')), 0, 10);
        $reference = app_normalize_whitespace((string) ($record['session_number'] ?? ''));
        $description = 'Auto posting for POS Session Cash Difference ' . $reference;
        $lines = app_build_pos_session_journal_lines($companyId, $record);
    } elseif ($slug === 'prepaid_expenses') {
        $entryDate = (string) ($record['prepaid_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['prepaid_number'] ?? ''));
        $description = 'Prepaid expense recognition ' . $reference;
        $lines = app_build_prepaid_expense_journal_lines($companyId, $record);
        $entryStatus = 'draft';
    } elseif ($slug === 'prepaid_amortization_runs') {
        $entryDate = (string) ($record['run_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['run_number'] ?? ''));
        $description = 'Prepaid expense amortization ' . $reference;
        $lines = app_build_prepaid_amortization_journal_lines($companyId, $record);
        $entryStatus = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft'))) === 'posted' ? 'posted' : 'draft';
    } elseif ($slug === 'depreciation_runs') {
        $entryDate = (string) ($record['run_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['run_number'] ?? ''));
        $description = 'Automatic depreciation run ' . $reference;
        $entryStatus = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft'))) === 'posted' ? 'posted' : 'draft';
        $totalAmount = max(0, app_number($record['total_amount'] ?? 0));
        $expenseCode = app_resolve_account_code(
            $companyId,
            (string) ($record['expense_account_code'] ?? ''),
            'expense',
            ['5600', '5500', '5210']
        );
        $accumulatedCode = app_resolve_account_code(
            $companyId,
            (string) ($record['accumulated_account_code'] ?? ''),
            'asset',
            ['1220', '1210', '1200']
        );
        $costCenterCode = app_normalize_whitespace((string) ($record['cost_center_code'] ?? ''));
        if ($totalAmount > 0) {
            $lines = [
                app_accounting_line($expenseCode, $totalAmount, 0, 'Depreciation expense for run ' . $reference, $costCenterCode),
                app_accounting_line($accumulatedCode, 0, $totalAmount, 'Accumulated depreciation for run ' . $reference),
            ];
        }
    } elseif ($slug === 'ifrs16_lease_schedules') {
        $entryDate = (string) ($record['schedule_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['lease_number'] ?? ''));
        $sequenceNo = app_integer($record['sequence_no'] ?? 0);
        if ($sequenceNo > 0) {
            $reference .= ($reference !== '' ? '-' : '') . str_pad((string) $sequenceNo, 3, '0', STR_PAD_LEFT);
        }
        $description = 'IFRS16 recurring lease entry ' . $reference;
        $entryStatus = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft'))) === 'posted' ? 'posted' : 'draft';
        $lines = app_build_ifrs16_schedule_journal_lines($companyId, $record);
    } elseif ($slug === 'excellence_operations') {
        $entryDate = (string) ($record['operation_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['operation_number'] ?? ''));
        $description = 'Auto posting for EXcellence operation ' . $reference;
        $lines = app_build_excellence_operation_journal_lines($companyId, $record);
    } elseif ($slug === 'excellence_hospital_expenses') {
        $entryDate = (string) ($record['expense_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['expense_number'] ?? ''));
        $description = 'Auto posting for EXcellence hospital expense ' . $reference;
        $lines = app_build_excellence_hospital_expense_journal_lines($companyId, $record);
    } elseif ($slug === 'excellence_admin_expenses') {
        $entryDate = (string) ($record['expense_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['expense_number'] ?? ''));
        $description = 'Auto posting for EXcellence administrative expense ' . $reference;
        $lines = app_build_excellence_paid_expense_journal_lines($companyId, $record, 'expense_number', 'EXcellence administrative expense', 'expense_account_code');
    } elseif ($slug === 'excellence_salaries') {
        $entryDate = (string) ($record['salary_date'] ?? date('Y-m-d'));
        $reference = app_normalize_whitespace((string) ($record['salary_number'] ?? ''));
        $description = 'Auto posting for EXcellence salary ' . $reference;
        $lines = app_build_excellence_paid_expense_journal_lines($companyId, $record, 'salary_number', 'EXcellence salary', 'salary_expense_account_code');
    }

    app_upsert_auto_journal_entry(
        $companyId,
        $slug,
        $sourceId,
        $entryDate,
        $reference,
        $description,
        $lines,
        $entryStatus
    );

    if (in_array($slug, ['depreciation_runs', 'prepaid_amortization_runs', 'ifrs16_lease_schedules'], true)) {
        $journalEntry = app_journal_entry_for_source($companyId, $slug, $sourceId);
        $journalEntryId = (int) ($journalEntry['id'] ?? 0);
        if ($journalEntryId > 0) {
            $table = match ($slug) {
                'depreciation_runs' => 'depreciation_runs',
                'ifrs16_lease_schedules' => 'ifrs16_lease_schedules',
                default => 'prepaid_amortization_runs',
            };
            app_pdo()->prepare(
                'UPDATE `' . $table . '`
                 SET journal_entry_id = :journal_entry_id
                 WHERE id = :id
                   AND company_id = :company_id'
            )->execute([
                'journal_entry_id' => $journalEntryId,
                'id' => $sourceId,
                'company_id' => $companyId,
            ]);
        }
    }
}

function app_month_period_date(string $date, int $monthOffset = 0): string
{
    $base = DateTimeImmutable::createFromFormat('Y-m-d', app_sanitize_date($date, date('Y-m-d')));
    if (!$base) {
        $base = new DateTimeImmutable(date('Y-m-d'));
    }
    $firstOfMonth = $base->modify('first day of this month');
    if ($monthOffset !== 0) {
        $firstOfMonth = $firstOfMonth->modify(($monthOffset > 0 ? '+' : '') . $monthOffset . ' months');
    }
    return $firstOfMonth->format('Y-m-d');
}

function app_month_number_from_start(string $startDate, string $periodDate): int
{
    $start = DateTimeImmutable::createFromFormat('Y-m-d', app_month_period_date($startDate));
    $period = DateTimeImmutable::createFromFormat('Y-m-d', app_month_period_date($periodDate));
    if (!$start || !$period) {
        return 1;
    }
    $months = ((int) $period->format('Y') - (int) $start->format('Y')) * 12
        + ((int) $period->format('n') - (int) $start->format('n'));
    return max(1, $months + 1);
}

function app_refresh_prepaid_amortized_amount(int $companyId, int $prepaidExpenseId): float
{
    if ($companyId <= 0 || $prepaidExpenseId <= 0) {
        return 0.0;
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        "SELECT COALESCE(SUM(amount), 0)
         FROM prepaid_amortization_runs
         WHERE company_id = :company_id
           AND prepaid_expense_id = :prepaid_expense_id
           AND status <> 'cancelled'"
    );
    $stmt->execute([
        'company_id' => $companyId,
        'prepaid_expense_id' => $prepaidExpenseId,
    ]);
    $amount = round(max(0, app_number($stmt->fetchColumn())), 2);

    $pdo->prepare(
        'UPDATE prepaid_expenses
         SET amortized_amount = :amortized_amount
         WHERE id = :id
           AND company_id = :company_id'
    )->execute([
        'amortized_amount' => $amount,
        'id' => $prepaidExpenseId,
        'company_id' => $companyId,
    ]);

    return $amount;
}

function app_run_prepaid_amortization_cycle(int $companyId, array $input): array
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for prepaid amortization.');
    }

    $prepaidExpenseId = app_integer($input['prepaid_expense_id'] ?? 0);
    $prepaid = app_prepaid_expense_by_id($companyId, $prepaidExpenseId);
    if (!$prepaid) {
        throw new RuntimeException('Select a valid prepaid expense.');
    }

    $prepaidStatus = strtolower(app_normalize_whitespace((string) ($prepaid['status'] ?? 'draft')));
    if (in_array($prepaidStatus, ['draft', 'cancelled'], true)) {
        throw new RuntimeException('Prepaid expense must be confirmed before generating amortization.');
    }

    $runDate = app_sanitize_date((string) ($input['run_date'] ?? ($prepaid['start_date'] ?? date('Y-m-01'))), date('Y-m-01'));
    $runDate = app_month_period_date($runDate);
    $durationMonths = max(1, app_integer($input['duration_months'] ?? 1));
    $durationMonths = min(360, $durationMonths);

    $expenseAccountCode = app_resolve_account_code(
        $companyId,
        (string) ($input['expense_account_code'] ?? ($prepaid['expense_account_code'] ?? '')),
        'expense',
        ['5310', '5210', '5510']
    );
    $prepaidAccountCode = app_resolve_account_code(
        $companyId,
        (string) ($input['prepaid_account_code'] ?? ($prepaid['prepaid_account_code'] ?? '')),
        'asset',
        ['1150', '1100']
    );
    $costCenterCode = app_normalize_whitespace((string) ($input['cost_center_code'] ?? ($prepaid['cost_center_code'] ?? '')));
    if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
        throw new RuntimeException('Select a valid cost center for prepaid amortization.');
    }

    $notes = app_normalize_whitespace((string) ($input['notes'] ?? ''));
    $totalAmount = round(max(0, app_number($prepaid['total_amount'] ?? 0)), 2);
    $monthlyAmount = round(max(0, app_number($prepaid['monthly_amount'] ?? 0)), 2);
    if ($monthlyAmount <= 0) {
        $prepaidDuration = max(1, app_integer($prepaid['duration_months'] ?? 1));
        $monthlyAmount = round($totalAmount / $prepaidDuration, 2);
    }
    if ($totalAmount <= 0 || $monthlyAmount <= 0) {
        throw new RuntimeException('Prepaid expense has no amortizable amount.');
    }

    $pdo = app_pdo();
    $startedTransaction = false;
    $created = [];
    $skipped = 0;
    $createdTotal = 0.0;

    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        $currentAmortized = app_refresh_prepaid_amortized_amount($companyId, $prepaidExpenseId);
        for ($offset = 0; $offset < $durationMonths; $offset++) {
            $periodDate = app_month_period_date($runDate, $offset);
            $periodMonth = (int) date('n', strtotime($periodDate));
            $periodYear = (int) date('Y', strtotime($periodDate));

            $existsStmt = $pdo->prepare(
                'SELECT run_number
                 FROM prepaid_amortization_runs
                 WHERE company_id = :company_id
                   AND prepaid_expense_id = :prepaid_expense_id
                   AND period_month = :period_month
                   AND period_year = :period_year
                 LIMIT 1'
            );
            $existsStmt->execute([
                'company_id' => $companyId,
                'prepaid_expense_id' => $prepaidExpenseId,
                'period_month' => $periodMonth,
                'period_year' => $periodYear,
            ]);
            if ($existsStmt->fetchColumn()) {
                $skipped++;
                continue;
            }

            $remaining = round(max(0, $totalAmount - $currentAmortized - $createdTotal), 2);
            if ($remaining <= 0) {
                break;
            }
            $amount = round(min($monthlyAmount, $remaining), 2);
            if ($amount <= 0) {
                break;
            }

            $runNumber = app_next_document_number('prepaid_amortization_runs', $companyId) ?? ('PAM-' . $periodYear . str_pad((string) $periodMonth, 2, '0', STR_PAD_LEFT));
            $monthNo = app_month_number_from_start((string) ($prepaid['start_date'] ?? $periodDate), $periodDate);
            $insertStmt = $pdo->prepare(
                'INSERT INTO prepaid_amortization_runs
                 (company_id, run_number, run_date, period_month, period_year, prepaid_expense_id, prepaid_number, month_no, prepaid_account_code, expense_account_code, cost_center_code, amount, status, notes)
                 VALUES
                 (:company_id, :run_number, :run_date, :period_month, :period_year, :prepaid_expense_id, :prepaid_number, :month_no, :prepaid_account_code, :expense_account_code, :cost_center_code, :amount, :status, :notes)'
            );
            $insertStmt->execute([
                'company_id' => $companyId,
                'run_number' => $runNumber,
                'run_date' => $periodDate,
                'period_month' => $periodMonth,
                'period_year' => $periodYear,
                'prepaid_expense_id' => $prepaidExpenseId,
                'prepaid_number' => (string) ($prepaid['prepaid_number'] ?? ''),
                'month_no' => $monthNo,
                'prepaid_account_code' => $prepaidAccountCode,
                'expense_account_code' => $expenseAccountCode,
                'cost_center_code' => $costCenterCode,
                'amount' => $amount,
                'status' => 'draft',
                'notes' => $notes,
            ]);
            $runId = (int) $pdo->lastInsertId();

            $runRecord = app_module_find('prepaid_amortization_runs', $runId, $companyId);
            if ($runRecord) {
                app_upsert_auto_journal_entry(
                    $companyId,
                    'prepaid_amortization_runs',
                    $runId,
                    $periodDate,
                    $runNumber,
                    'Prepaid expense amortization ' . $runNumber,
                    app_build_prepaid_amortization_journal_lines($companyId, $runRecord),
                    'draft'
                );
                $journalEntry = app_journal_entry_for_source($companyId, 'prepaid_amortization_runs', $runId);
                $journalEntryId = (int) ($journalEntry['id'] ?? 0);
                if ($journalEntryId > 0) {
                    $pdo->prepare(
                        'UPDATE prepaid_amortization_runs
                         SET journal_entry_id = :journal_entry_id
                         WHERE id = :id
                           AND company_id = :company_id'
                    )->execute([
                        'journal_entry_id' => $journalEntryId,
                        'id' => $runId,
                        'company_id' => $companyId,
                    ]);
                }
            }

            $created[] = [
                'run_id' => $runId,
                'run_number' => $runNumber,
                'amount' => $amount,
            ];
            $createdTotal = round($createdTotal + $amount, 2);
        }

        if (!$created) {
            throw new RuntimeException($skipped > 0 ? 'Selected prepaid periods already exist.' : 'No remaining prepaid amount to amortize.');
        }

        app_refresh_prepaid_amortized_amount($companyId, $prepaidExpenseId);

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }

        $last = end($created) ?: [];
        return [
            'run_id' => (int) ($last['run_id'] ?? 0),
            'run_number' => (string) ($last['run_number'] ?? ''),
            'runs_count' => count($created),
            'skipped_count' => $skipped,
            'total_amount' => $createdTotal,
        ];
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_asset_monthly_depreciation_amount(array $asset, string $runDate): float
{
    $purchaseDate = app_sanitize_date((string) ($asset['purchase_date'] ?? ''), '1900-01-01');
    $runDate = app_sanitize_date($runDate, date('Y-m-d'));
    if ((int) ($asset['auto_depreciation_enabled'] ?? 1) !== 1) {
        return 0.0;
    }

    $depreciationStartDate = app_sanitize_date((string) ($asset['depreciation_start_date'] ?? ''), $purchaseDate);
    $effectiveStartDate = $depreciationStartDate > $purchaseDate ? $depreciationStartDate : $purchaseDate;
    if ($effectiveStartDate > $runDate) {
        return 0.0;
    }

    $purchaseCost = max(0, app_number($asset['purchase_cost'] ?? 0));
    $salvageValue = max(0, app_number($asset['salvage_value'] ?? 0));
    $depreciableBase = max(0, $purchaseCost - $salvageValue);
    $scheduleMonths = max(0, (int) ($asset['depreciation_months'] ?? 0));
    $usefulLifeMonths = $scheduleMonths > 0 ? $scheduleMonths : max(0, (int) ($asset['useful_life_months'] ?? 0));
    $accumulated = max(0, app_number($asset['accumulated_depreciation'] ?? 0));
    $remaining = max(0, $depreciableBase - $accumulated);
    if ($depreciableBase <= 0 || $usefulLifeMonths <= 0 || $remaining <= 0) {
        return 0.0;
    }

    if ($scheduleMonths > 0) {
        $startMonth = strtotime(date('Y-m-01', strtotime($effectiveStartDate)));
        $runMonth = strtotime(date('Y-m-01', strtotime($runDate)));
        if ($startMonth === false || $runMonth === false) {
            return 0.0;
        }
        $elapsedMonths = ((int) date('Y', $runMonth) - (int) date('Y', $startMonth)) * 12
            + ((int) date('n', $runMonth) - (int) date('n', $startMonth));
        if ($elapsedMonths < 0 || $elapsedMonths >= $scheduleMonths) {
            return 0.0;
        }
    }

    $method = strtolower(app_normalize_whitespace((string) ($asset['depreciation_method'] ?? 'straight_line')));
    $monthlyAmount = 0.0;
    if ($method === 'declining') {
        $bookValue = max(0, $purchaseCost - $accumulated);
        $annualRate = min(1.0, max(0.01, 2 / max(1, $usefulLifeMonths)));
        $monthlyAmount = ($bookValue * $annualRate) / 12;
    } else {
        $monthlyAmount = $depreciableBase / max(1, $usefulLifeMonths);
    }

    $monthlyAmount = round(max(0, $monthlyAmount), 2);
    if ($monthlyAmount <= 0) {
        return 0.0;
    }
    if ($monthlyAmount > $remaining) {
        $monthlyAmount = round($remaining, 2);
    }
    return $monthlyAmount;
}

function app_run_depreciation_cycle(int $companyId, array $input): array
{
    if ($companyId <= 0) {
        throw new RuntimeException('Company context is required for depreciation run.');
    }

    $scheduleStartDateRaw = app_normalize_whitespace((string) ($input['depreciation_start_date'] ?? ($input['schedule_start_date'] ?? '')));
    $runDate = app_sanitize_date(
        $scheduleStartDateRaw !== '' ? $scheduleStartDateRaw : (string) ($input['run_date'] ?? date('Y-m-d')),
        date('Y-m-d')
    );
    $runDate = app_month_period_date($runDate);
    $assetId = app_integer($input['asset_id'] ?? 0);
    $durationMonths = max(1, app_integer($input['depreciation_months'] ?? ($input['duration_months'] ?? 1)));
    $durationMonths = min(360, $durationMonths);
    $costCenterCode = app_normalize_whitespace((string) ($input['cost_center_code'] ?? ''));
    if ($costCenterCode === '' || !app_cost_center_exists($companyId, $costCenterCode)) {
        throw new RuntimeException('Select a valid cost center for depreciation posting.');
    }

    $expenseAccountCode = app_resolve_account_code(
        $companyId,
        (string) ($input['expense_account_code'] ?? ''),
        'expense',
        ['5600', '5500', '5210']
    );
    $accumulatedAccountCode = app_resolve_account_code(
        $companyId,
        (string) ($input['accumulated_account_code'] ?? ''),
        'asset',
        ['1220', '1210', '1200']
    );
    $notes = app_normalize_whitespace((string) ($input['notes'] ?? ''));

    $pdo = app_pdo();
    $startedTransaction = false;
    $created = [];
    $skipped = 0;
    $createdTotal = 0.0;
    $assetsCountTotal = 0;
    $lastJournalEntryId = 0;
    $lastJournalEntryNumber = '';

    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        $assetFilterSql = '';
        $assetParams = [
            'company_id' => $companyId,
            'status' => 'active',
        ];
        if ($assetId > 0) {
            $assetFilterSql = ' AND id = :asset_id';
            $assetParams['asset_id'] = $assetId;
        }

        $assetsStmt = $pdo->prepare(
            'SELECT id, asset_code, asset_name, purchase_date, purchase_cost, useful_life_months, salvage_value, depreciation_method,
                    auto_depreciation_enabled, depreciation_start_date, depreciation_months, depreciation_expense_account_code,
                    depreciation_accumulated_account_code, accumulated_depreciation, status, cost_center_code
             FROM assets
             WHERE company_id = :company_id
               AND status = :status' . $assetFilterSql . '
             ORDER BY asset_code ASC, id ASC'
        );
        $insertRunStmt = $pdo->prepare(
            'INSERT INTO depreciation_runs
             (company_id, run_number, run_date, period_month, period_year, asset_id, asset_code, schedule_start_date, duration_months, cost_center_code, expense_account_code, accumulated_account_code, total_amount, assets_count, status, notes)
              VALUES
               (:company_id, :run_number, :run_date, :period_month, :period_year, :asset_id, :asset_code, :schedule_start_date, :duration_months, :cost_center_code, :expense_account_code, :accumulated_account_code, :total_amount, :assets_count, :status, :notes)'
        );
        $insertLineStmt = $pdo->prepare(
            'INSERT INTO depreciation_run_lines
             (company_id, run_id, asset_id, asset_code, asset_name, cost_center_code, depreciation_amount, previous_accumulated, new_accumulated)
             VALUES
              (:company_id, :run_id, :asset_id, :asset_code, :asset_name, :cost_center_code, :depreciation_amount, :previous_accumulated, :new_accumulated)'
        );
        $updateAssetStmt = $pdo->prepare(
            'UPDATE assets
             SET accumulated_depreciation = :new_accumulated
             WHERE id = :id
               AND company_id = :company_id'
        );

        $existsSelectedStmt = $pdo->prepare(
            'SELECT r.run_number
             FROM depreciation_runs r
             INNER JOIN depreciation_run_lines l
                ON l.company_id = r.company_id
               AND l.run_id = r.id
             WHERE r.company_id = :company_id
               AND r.period_month = :period_month
               AND r.period_year = :period_year
               AND l.asset_id = :asset_id
             LIMIT 1'
        );
        $existsAllStmt = $pdo->prepare(
            'SELECT run_number
             FROM depreciation_runs
             WHERE company_id = :company_id
               AND period_month = :period_month
               AND period_year = :period_year
             LIMIT 1'
        );

        for ($offset = 0; $offset < $durationMonths; $offset++) {
            $periodDate = app_month_period_date($runDate, $offset);
            $periodMonth = (int) date('n', strtotime($periodDate));
            $periodYear = (int) date('Y', strtotime($periodDate));

            if ($assetId > 0) {
                $existsSelectedStmt->execute([
                    'company_id' => $companyId,
                    'period_month' => $periodMonth,
                    'period_year' => $periodYear,
                    'asset_id' => $assetId,
                ]);
                $existingRunNumber = app_normalize_whitespace((string) ($existsSelectedStmt->fetchColumn() ?: ''));
            } else {
                $existsAllStmt->execute([
                    'company_id' => $companyId,
                    'period_month' => $periodMonth,
                    'period_year' => $periodYear,
                ]);
                $existingRunNumber = app_normalize_whitespace((string) ($existsAllStmt->fetchColumn() ?: ''));
            }
            if ($existingRunNumber !== '') {
                $skipped++;
                continue;
            }

            $assetsStmt->execute($assetParams);
            $assets = $assetsStmt->fetchAll() ?: [];
            if (!$assets) {
                if ($assetId > 0 && $offset === 0) {
                    throw new RuntimeException('Selected asset is not active or was not found.');
                }
                continue;
            }

            $runLines = [];
            $totalAmount = 0.0;
            foreach ($assets as $asset) {
                if ($assetId > 0) {
                    $asset['auto_depreciation_enabled'] = 1;
                    $asset['depreciation_start_date'] = $runDate;
                    $asset['depreciation_months'] = $durationMonths;
                }
                $amount = app_asset_monthly_depreciation_amount($asset, $periodDate);
                if ($amount <= 0) {
                    continue;
                }

                $assetCostCenter = app_normalize_whitespace((string) ($asset['cost_center_code'] ?? ''));
                if ($assetCostCenter === '' || !app_cost_center_exists($companyId, $assetCostCenter)) {
                    $assetCostCenter = $costCenterCode;
                }

                $previousAccumulated = max(0, app_number($asset['accumulated_depreciation'] ?? 0));
                $newAccumulated = round($previousAccumulated + $amount, 2);

                $updateAssetStmt->execute([
                    'new_accumulated' => $newAccumulated,
                    'id' => (int) ($asset['id'] ?? 0),
                    'company_id' => $companyId,
                ]);

                $runLines[] = [
                    'asset_id' => (int) ($asset['id'] ?? 0),
                    'asset_code' => (string) ($asset['asset_code'] ?? ''),
                    'asset_name' => (string) ($asset['asset_name'] ?? ''),
                    'cost_center_code' => $assetCostCenter,
                    'depreciation_amount' => $amount,
                    'previous_accumulated' => $previousAccumulated,
                    'new_accumulated' => $newAccumulated,
                ];
                $totalAmount += $amount;
            }

            $totalAmount = round($totalAmount, 2);
            if ($totalAmount <= 0 || !$runLines) {
                continue;
            }

            $runAssetCode = '';
            if ($assetId > 0 && isset($assets[0])) {
                $runAssetCode = app_normalize_whitespace((string) ($assets[0]['asset_code'] ?? ''));
            }

            $runNumber = app_next_document_number('depreciation_runs', $companyId) ?? ('DEP-' . $periodYear . str_pad((string) $periodMonth, 2, '0', STR_PAD_LEFT));
            $insertRunStmt->execute([
                'company_id' => $companyId,
                'run_number' => $runNumber,
                'run_date' => $periodDate,
                'period_month' => $periodMonth,
                'period_year' => $periodYear,
                'asset_id' => $assetId > 0 ? $assetId : null,
                'asset_code' => $runAssetCode,
                'schedule_start_date' => $runDate,
                'duration_months' => $durationMonths,
                'cost_center_code' => $costCenterCode,
                'expense_account_code' => $expenseAccountCode,
                'accumulated_account_code' => $accumulatedAccountCode,
                'total_amount' => $totalAmount,
                'assets_count' => count($runLines),
                'status' => 'draft',
                'notes' => $notes,
            ]);
            $runId = (int) $pdo->lastInsertId();

            foreach ($runLines as $line) {
                $insertLineStmt->execute([
                    'company_id' => $companyId,
                    'run_id' => $runId,
                    'asset_id' => (int) ($line['asset_id'] ?? 0),
                    'asset_code' => (string) ($line['asset_code'] ?? ''),
                    'asset_name' => (string) ($line['asset_name'] ?? ''),
                    'cost_center_code' => (string) ($line['cost_center_code'] ?? ''),
                    'depreciation_amount' => round(app_number($line['depreciation_amount'] ?? 0), 2),
                    'previous_accumulated' => round(app_number($line['previous_accumulated'] ?? 0), 2),
                    'new_accumulated' => round(app_number($line['new_accumulated'] ?? 0), 2),
                ]);
            }

            $periodLabel = str_pad((string) $periodMonth, 2, '0', STR_PAD_LEFT) . '/' . $periodYear;
            $journalLines = [
                app_accounting_line($expenseAccountCode, $totalAmount, 0, 'Depreciation expense for period ' . $periodLabel, $costCenterCode),
                app_accounting_line($accumulatedAccountCode, 0, $totalAmount, 'Accumulated depreciation for period ' . $periodLabel),
            ];
            app_upsert_auto_journal_entry(
                $companyId,
                'depreciation_runs',
                $runId,
                $periodDate,
                $runNumber,
                'Automatic depreciation run ' . $runNumber,
                $journalLines,
                'draft'
            );

            $journalEntry = app_journal_entry_for_source($companyId, 'depreciation_runs', $runId);
            $journalEntryId = (int) ($journalEntry['id'] ?? 0);
            if ($journalEntryId > 0) {
                $pdo->prepare(
                    'UPDATE depreciation_runs
                     SET journal_entry_id = :journal_entry_id
                     WHERE id = :id
                       AND company_id = :company_id'
                )->execute([
                    'journal_entry_id' => $journalEntryId,
                    'id' => $runId,
                    'company_id' => $companyId,
                ]);
            }

            $created[] = [
                'run_id' => $runId,
                'run_number' => $runNumber,
                'amount' => $totalAmount,
                'assets_count' => count($runLines),
            ];
            $createdTotal = round($createdTotal + $totalAmount, 2);
            $assetsCountTotal += count($runLines);
            $lastJournalEntryId = $journalEntryId;
            $lastJournalEntryNumber = (string) ($journalEntry['entry_number'] ?? '');
        }

        if (!$created) {
            throw new RuntimeException($skipped > 0 ? 'Selected depreciation periods already exist.' : 'No depreciable amount found for the selected periods.');
        }

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }

        $last = end($created) ?: [];
        return [
            'run_id' => (int) ($last['run_id'] ?? 0),
            'run_number' => (string) ($last['run_number'] ?? ''),
            'runs_count' => count($created),
            'skipped_count' => $skipped,
            'total_amount' => $createdTotal,
            'assets_count' => $assetsCountTotal,
            'journal_entry_id' => $lastJournalEntryId,
            'journal_entry_number' => $lastJournalEntryNumber,
        ];
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_account_type_delta(string $accountType, float $debit, float $credit): float
{
    unset($accountType);

    // Reports must keep the same accounting direction as the journal lines:
    // debit balances are positive, credit balances are negative.
    return $debit - $credit;
}

function app_account_opening_signed(array $account): float
{
    $accountType = strtolower(app_normalize_whitespace((string) ($account['account_type'] ?? 'asset')));
    $opening = app_number($account['opening_balance'] ?? 0);
    if (in_array($accountType, ['asset', 'expense'], true)) {
        return $opening;
    }
    return -$opening;
}

function app_signed_balance_to_dr_cr(float $signedBalance): array
{
    if ($signedBalance >= 0) {
        return ['dr' => $signedBalance, 'cr' => 0.0];
    }
    return ['dr' => 0.0, 'cr' => abs($signedBalance)];
}

function app_signed_balance_label(float $signedBalance): string
{
    $balance = app_signed_balance_to_dr_cr($signedBalance);
    if ($balance['dr'] > 0) {
        return app_money_format($balance['dr']) . ' Dr';
    }
    if ($balance['cr'] > 0) {
        return app_money_format($balance['cr']) . ' Cr';
    }
    return '0';
}

function app_accounting_trial_balance(int $companyId, string $fromDate, string $toDate, bool $includeZero = false): array
{
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    $accounts = app_chart_accounts($companyId, null, false);
    $accountsByCode = [];
    foreach ($accounts as $account) {
        $code = app_normalize_whitespace((string) ($account['account_code'] ?? ''));
        if ($code !== '') {
            $accountsByCode[$code] = $account;
        }
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT
             l.account_code,
             SUM(CASE WHEN j.entry_date < :from_date_open_debit THEN l.debit ELSE 0 END) AS opening_debit,
             SUM(CASE WHEN j.entry_date < :from_date_open_credit THEN l.credit ELSE 0 END) AS opening_credit,
             SUM(CASE WHEN j.entry_date >= :from_date_period_debit AND j.entry_date <= :to_date_period_debit THEN l.debit ELSE 0 END) AS period_debit,
             SUM(CASE WHEN j.entry_date >= :from_date_period_credit AND j.entry_date <= :to_date_period_credit THEN l.credit ELSE 0 END) AS period_credit
         FROM journal_entry_lines l
         INNER JOIN journal_entries j ON j.id = l.journal_entry_id
         WHERE l.company_id = :company_id_lines
           AND j.company_id = :company_id_entries
           AND j.status = :status
           AND j.entry_date <= :to_date_filter
         GROUP BY l.account_code'
    );
    $stmt->execute([
        'company_id_lines' => $companyId,
        'company_id_entries' => $companyId,
        'status' => 'posted',
        'from_date_open_debit' => $fromDate,
        'from_date_open_credit' => $fromDate,
        'from_date_period_debit' => $fromDate,
        'to_date_period_debit' => $toDate,
        'from_date_period_credit' => $fromDate,
        'to_date_period_credit' => $toDate,
        'to_date_filter' => $toDate,
    ]);
    $txRows = $stmt->fetchAll() ?: [];

    $txByCode = [];
    foreach ($txRows as $tx) {
        $code = app_normalize_whitespace((string) ($tx['account_code'] ?? ''));
        if ($code !== '') {
            $txByCode[$code] = $tx;
            if (!isset($accountsByCode[$code])) {
                $accountsByCode[$code] = [
                    'account_code' => $code,
                    'account_name' => 'Unmapped Account (not in chart)',
                    'account_type' => 'unmapped',
                    'opening_balance' => 0,
                    'is_unmapped' => true,
                ];
            }
        }
    }

    $rows = [];
    $totals = [
        'opening_dr' => 0.0,
        'opening_cr' => 0.0,
        'period_debit' => 0.0,
        'period_credit' => 0.0,
        'closing_dr' => 0.0,
        'closing_cr' => 0.0,
    ];

    foreach ($accountsByCode as $code => $account) {
        $tx = $txByCode[$code] ?? [];
        $accountType = (string) ($account['account_type'] ?? 'asset');
        $openingSigned = app_account_opening_signed($account)
            + app_account_type_delta(
                $accountType,
                app_number($tx['opening_debit'] ?? 0),
                app_number($tx['opening_credit'] ?? 0)
            );
        $periodDebit = app_number($tx['period_debit'] ?? 0);
        $periodCredit = app_number($tx['period_credit'] ?? 0);
        $periodSigned = app_account_type_delta($accountType, $periodDebit, $periodCredit);
        $closingSigned = $openingSigned + $periodSigned;

        $opening = app_signed_balance_to_dr_cr($openingSigned);
        $closing = app_signed_balance_to_dr_cr($closingSigned);

        $isZero = abs($opening['dr']) < 0.0001
            && abs($opening['cr']) < 0.0001
            && abs($periodDebit) < 0.0001
            && abs($periodCredit) < 0.0001
            && abs($closing['dr']) < 0.0001
            && abs($closing['cr']) < 0.0001;
        if (!$includeZero && $isZero) {
            continue;
        }

        $row = [
            'account_code' => $code,
            'account_name' => (string) ($account['account_name'] ?? ''),
            'account_type' => $accountType,
            'financial_statement_group_code' => (string) ($account['financial_statement_group_code'] ?? ''),
            'opening_dr' => $opening['dr'],
            'opening_cr' => $opening['cr'],
            'period_debit' => $periodDebit,
            'period_credit' => $periodCredit,
            'period_signed' => $periodSigned,
            'closing_dr' => $closing['dr'],
            'closing_cr' => $closing['cr'],
            'closing_signed' => $closingSigned,
        ];
        $rows[] = $row;

        $totals['opening_dr'] += $row['opening_dr'];
        $totals['opening_cr'] += $row['opening_cr'];
        $totals['period_debit'] += $row['period_debit'];
        $totals['period_credit'] += $row['period_credit'];
        $totals['closing_dr'] += $row['closing_dr'];
        $totals['closing_cr'] += $row['closing_cr'];
    }

    $openingDifference = round($totals['opening_dr'] - $totals['opening_cr'], 2);
    if (abs($openingDifference) >= 0.01) {
        $differenceIsCredit = $openingDifference > 0;
        $differenceAmount = abs($openingDifference);
        $differenceRow = [
            'account_code' => '9999-OPEN-DIFF',
            'account_name' => 'Opening Balance Difference (review chart opening balances)',
            'account_type' => $differenceIsCredit ? 'equity' : 'asset',
            'opening_dr' => $differenceIsCredit ? 0.0 : $differenceAmount,
            'opening_cr' => $differenceIsCredit ? $differenceAmount : 0.0,
            'period_debit' => 0.0,
            'period_credit' => 0.0,
            'period_signed' => 0.0,
            'closing_dr' => $differenceIsCredit ? 0.0 : $differenceAmount,
            'closing_cr' => $differenceIsCredit ? $differenceAmount : 0.0,
            'closing_signed' => $differenceIsCredit ? -$differenceAmount : $differenceAmount,
            'is_system_difference' => true,
        ];
        $rows[] = $differenceRow;
        $totals['opening_dr'] += $differenceRow['opening_dr'];
        $totals['opening_cr'] += $differenceRow['opening_cr'];
        $totals['closing_dr'] += $differenceRow['closing_dr'];
        $totals['closing_cr'] += $differenceRow['closing_cr'];
    }

    usort(
        $rows,
        static fn (array $a, array $b): int => strcmp((string) ($a['account_code'] ?? ''), (string) ($b['account_code'] ?? ''))
    );

    return [
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'rows' => $rows,
        'totals' => $totals,
    ];
}

function app_accounting_ledger(int $companyId, string $accountCode, string $fromDate, string $toDate): array
{
    $accountCode = app_normalize_whitespace($accountCode);
    if ($companyId <= 0 || $accountCode === '') {
        return [
            'account' => null,
            'from_date' => app_sanitize_date($fromDate, date('Y-01-01')),
            'to_date' => app_sanitize_date($toDate, date('Y-m-d')),
            'opening_signed' => 0.0,
            'closing_signed' => 0.0,
            'rows' => [],
            'totals' => ['debit' => 0.0, 'credit' => 0.0],
        ];
    }

    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    $pdo = app_pdo();
    $accountStmt = $pdo->prepare(
        'SELECT account_code, account_name, account_type, opening_balance
         FROM chart_of_accounts
         WHERE company_id = :company_id AND account_code = :account_code
         LIMIT 1'
    );
    $accountStmt->execute([
        'company_id' => $companyId,
        'account_code' => $accountCode,
    ]);
    $account = $accountStmt->fetch() ?: null;
    if (!$account) {
        return [
            'account' => null,
            'from_date' => $fromDate,
            'to_date' => $toDate,
            'opening_signed' => 0.0,
            'closing_signed' => 0.0,
            'rows' => [],
            'totals' => ['debit' => 0.0, 'credit' => 0.0],
        ];
    }

    $openingStmt = $pdo->prepare(
        'SELECT COALESCE(SUM(l.debit), 0) AS total_debit, COALESCE(SUM(l.credit), 0) AS total_credit
         FROM journal_entry_lines l
         INNER JOIN journal_entries j ON j.id = l.journal_entry_id
         WHERE l.company_id = :company_id_lines
           AND j.company_id = :company_id_entries
           AND l.account_code = :account_code
           AND j.status = :status
           AND j.entry_date < :from_date'
    );
    $openingStmt->execute([
        'company_id_lines' => $companyId,
        'company_id_entries' => $companyId,
        'account_code' => $accountCode,
        'status' => 'posted',
        'from_date' => $fromDate,
    ]);
    $openingTx = $openingStmt->fetch() ?: [];
    $openingSigned = app_account_opening_signed($account)
        + app_account_type_delta(
            (string) ($account['account_type'] ?? 'asset'),
            app_number($openingTx['total_debit'] ?? 0),
            app_number($openingTx['total_credit'] ?? 0)
        );

    $rowsStmt = $pdo->prepare(
        'SELECT
             j.entry_date,
             j.entry_number,
             j.reference,
             j.description AS entry_description,
             l.description AS line_description,
             l.debit,
             l.credit,
             l.id AS line_id
         FROM journal_entry_lines l
         INNER JOIN journal_entries j ON j.id = l.journal_entry_id
         WHERE l.company_id = :company_id_lines
           AND j.company_id = :company_id_entries
           AND l.account_code = :account_code
           AND j.status = :status
           AND j.entry_date >= :from_date
           AND j.entry_date <= :to_date
         ORDER BY j.entry_date ASC, j.id ASC, l.line_no ASC, l.id ASC'
    );
    $rowsStmt->execute([
        'company_id_lines' => $companyId,
        'company_id_entries' => $companyId,
        'account_code' => $accountCode,
        'status' => 'posted',
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ]);
    $txRows = $rowsStmt->fetchAll() ?: [];

    $running = $openingSigned;
    $rows = [];
    $totals = ['debit' => 0.0, 'credit' => 0.0];
    if (abs($openingSigned) > 0.0001) {
        $rows[] = [
            'entry_date' => $fromDate,
            'entry_number' => 'OPENING',
            'reference' => 'Opening Balance',
            'description' => 'Opening balance before ' . $fromDate,
            'debit' => 0.0,
            'credit' => 0.0,
            'running_signed' => round($openingSigned, 2),
            'is_opening' => true,
        ];
    }
    foreach ($txRows as $tx) {
        $debit = app_number($tx['debit'] ?? 0);
        $credit = app_number($tx['credit'] ?? 0);
        $totals['debit'] += $debit;
        $totals['credit'] += $credit;
        $running += app_account_type_delta((string) ($account['account_type'] ?? 'asset'), $debit, $credit);

        $rows[] = [
            'entry_date' => (string) ($tx['entry_date'] ?? ''),
            'entry_number' => (string) ($tx['entry_number'] ?? ''),
            'reference' => (string) ($tx['reference'] ?? ''),
            'description' => app_normalize_whitespace((string) (($tx['line_description'] ?? '') !== '' ? $tx['line_description'] : $tx['entry_description'])),
            'debit' => $debit,
            'credit' => $credit,
            'running_signed' => $running,
        ];
    }

    return [
        'account' => $account,
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'opening_signed' => $openingSigned,
        'closing_signed' => $running,
        'rows' => $rows,
        'totals' => $totals,
    ];
}

function app_accounting_contact_statement(int $companyId, string $contactCode, string $fromDate, string $toDate): array
{
    $contactCode = app_normalize_whitespace($contactCode);
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    $empty = [
        'contact' => null,
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'opening_signed' => 0.0,
        'closing_signed' => 0.0,
        'rows' => [],
        'totals' => ['debit' => 0.0, 'credit' => 0.0],
    ];

    if ($companyId <= 0 || $contactCode === '') {
        return $empty;
    }

    $pdo = app_pdo();
    $contactStmt = $pdo->prepare(
        'SELECT contact_code, contact_name, contact_type, opening_balance
         FROM contacts
         WHERE company_id = :company_id
           AND contact_code = :contact_code
         LIMIT 1'
    );
    $contactStmt->execute([
        'company_id' => $companyId,
        'contact_code' => $contactCode,
    ]);
    $contact = $contactStmt->fetch() ?: null;
    if (!$contact) {
        return $empty;
    }

    $openingSigned = app_number($contact['opening_balance'] ?? 0);
    $rows = [];
    $totals = ['debit' => 0.0, 'credit' => 0.0];

    $appendRow = static function (array &$target, string $date, string $type, string $reference, string $description, float $debit, float $credit, int $sourceId = 0, string $sourceModule = ''): void {
        $target[] = [
            'date' => $date,
            'type' => $type,
            'reference' => $reference,
            'description' => $description,
            'debit' => round(max(0, $debit), 2),
            'credit' => round(max(0, $credit), 2),
            'source_id' => $sourceId,
            'source_module' => $sourceModule,
        ];
    };

    $salesStmt = $pdo->prepare(
        'SELECT id, invoice_date AS tx_date, invoice_number AS reference, grand_total, status
         FROM sales_invoices
         WHERE company_id = :company_id
           AND customer_code = :contact_code
           AND status NOT IN (:draft_status, :cancelled_status)
           AND invoice_date <= :to_date'
    );
    $salesStmt->execute([
        'company_id' => $companyId,
        'contact_code' => $contactCode,
        'draft_status' => 'draft',
        'cancelled_status' => 'cancelled',
        'to_date' => $toDate,
    ]);
    foreach ($salesStmt->fetchAll() ?: [] as $row) {
        $date = app_sanitize_date((string) ($row['tx_date'] ?? ''), $toDate);
        $amount = max(0, app_number($row['grand_total'] ?? 0));
        if ($date < $fromDate) {
            $openingSigned += $amount;
            continue;
        }
        $appendRow($rows, $date, 'Sales Invoice', (string) ($row['reference'] ?? ''), 'Sales invoice posted to contact', $amount, 0.0, (int) ($row['id'] ?? 0), 'sales_invoices');
        $totals['debit'] += $amount;
    }

    $purchaseStmt = $pdo->prepare(
        'SELECT id, bill_date AS tx_date, bill_number AS reference, grand_total, status
         FROM purchase_bills
         WHERE company_id = :company_id
           AND vendor_code = :contact_code
           AND status NOT IN (:draft_status, :cancelled_status)
           AND bill_date <= :to_date'
    );
    $purchaseStmt->execute([
        'company_id' => $companyId,
        'contact_code' => $contactCode,
        'draft_status' => 'draft',
        'cancelled_status' => 'cancelled',
        'to_date' => $toDate,
    ]);
    foreach ($purchaseStmt->fetchAll() ?: [] as $row) {
        $date = app_sanitize_date((string) ($row['tx_date'] ?? ''), $toDate);
        $amount = max(0, app_number($row['grand_total'] ?? 0));
        if ($date < $fromDate) {
            $openingSigned -= $amount;
            continue;
        }
        $appendRow($rows, $date, 'Purchase Bill', (string) ($row['reference'] ?? ''), 'Vendor bill posted to contact', 0.0, $amount, (int) ($row['id'] ?? 0), 'purchase_bills');
        $totals['credit'] += $amount;
    }

    $paymentStmt = $pdo->prepare(
        'SELECT id, payment_date AS tx_date, payment_number AS reference, direction, amount, reference AS payment_reference
         FROM payments
         WHERE company_id = :company_id
           AND contact_code = :contact_code
           AND payment_date <= :to_date'
    );
    $paymentStmt->execute([
        'company_id' => $companyId,
        'contact_code' => $contactCode,
        'to_date' => $toDate,
    ]);
    foreach ($paymentStmt->fetchAll() ?: [] as $row) {
        $date = app_sanitize_date((string) ($row['tx_date'] ?? ''), $toDate);
        $amount = max(0, app_number($row['amount'] ?? 0));
        $direction = strtolower(app_normalize_whitespace((string) ($row['direction'] ?? '')));
        $debit = $direction === 'out' ? $amount : 0.0;
        $credit = $direction === 'out' ? 0.0 : $amount;
        if ($date < $fromDate) {
            $openingSigned += $debit - $credit;
            continue;
        }
        $appendRow($rows, $date, $direction === 'out' ? 'Outgoing Payment' : 'Incoming Payment', (string) ($row['reference'] ?? ''), (string) ($row['payment_reference'] ?? 'Payment'), $debit, $credit, (int) ($row['id'] ?? 0), 'payments');
        $totals['debit'] += $debit;
        $totals['credit'] += $credit;
    }

    usort(
        $rows,
        static function (array $a, array $b): int {
            $dateCompare = strcmp((string) ($a['date'] ?? ''), (string) ($b['date'] ?? ''));
            if ($dateCompare !== 0) {
                return $dateCompare;
            }
            return strcmp((string) ($a['reference'] ?? ''), (string) ($b['reference'] ?? ''));
        }
    );

    $running = $openingSigned;
    if (abs($openingSigned) > 0.0001) {
        array_unshift($rows, [
            'date' => $fromDate,
            'type' => 'Opening Balance',
            'reference' => 'OPENING',
            'description' => 'Opening balance before ' . $fromDate,
            'debit' => 0.0,
            'credit' => 0.0,
            'source_id' => 0,
            'source_module' => '',
            'running_signed' => round($openingSigned, 2),
            'source_url' => null,
            'is_opening' => true,
        ]);
    }
    foreach ($rows as &$row) {
        $running += app_number($row['debit'] ?? 0) - app_number($row['credit'] ?? 0);
        $row['running_signed'] = round($running, 2);
        $row['source_url'] = isset($row['source_url'])
            ? $row['source_url']
            : app_source_module_url((string) ($row['source_module'] ?? ''), (int) ($row['source_id'] ?? 0), $companyId);
    }
    unset($row);

    return [
        'contact' => $contact,
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'opening_signed' => round($openingSigned, 2),
        'closing_signed' => round($running, 2),
        'rows' => $rows,
        'totals' => [
            'debit' => round($totals['debit'], 2),
            'credit' => round($totals['credit'], 2),
        ],
    ];
}

function app_accounting_journal_book(int $companyId, string $fromDate, string $toDate): array
{
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    if ($companyId <= 0) {
        return [
            'from_date' => $fromDate,
            'to_date' => $toDate,
            'rows' => [],
            'totals' => ['debit' => 0.0, 'credit' => 0.0],
        ];
    }

    $stmt = app_pdo()->prepare(
        'SELECT
             j.id AS journal_entry_id,
             j.entry_date,
             j.entry_number,
             j.reference,
             j.source_type,
             j.source_id,
             l.line_no,
             l.account_code,
             COALESCE(c.account_name, l.account_code) AS account_name,
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             l.description AS line_description,
             j.description AS entry_description,
             l.debit,
             l.credit
         FROM journal_entries j
         INNER JOIN journal_entry_lines l ON l.journal_entry_id = j.id
         LEFT JOIN chart_of_accounts c ON c.company_id = j.company_id AND c.account_code = l.account_code
         LEFT JOIN cost_centers cc ON cc.company_id = l.company_id AND cc.center_code = l.cost_center_code
         WHERE j.company_id = :company_id_entries
           AND l.company_id = :company_id_lines
           AND j.status = :status
           AND j.entry_date >= :from_date
           AND j.entry_date <= :to_date
         ORDER BY j.entry_date ASC, j.id ASC, l.line_no ASC, l.id ASC'
    );
    $stmt->execute([
        'company_id_entries' => $companyId,
        'company_id_lines' => $companyId,
        'status' => 'posted',
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ]);
    $rows = $stmt->fetchAll() ?: [];

    $totals = ['debit' => 0.0, 'credit' => 0.0];
    $resultRows = [];
    foreach ($rows as $row) {
        $debit = app_number($row['debit'] ?? 0);
        $credit = app_number($row['credit'] ?? 0);
        $totals['debit'] += $debit;
        $totals['credit'] += $credit;

        $sourceType = app_normalize_whitespace((string) ($row['source_type'] ?? ''));
        $sourceId = (int) ($row['source_id'] ?? 0);
        $sourceUrl = app_source_module_url($sourceType, $sourceId, $companyId);

        $resultRows[] = [
            'journal_entry_id' => (int) ($row['journal_entry_id'] ?? 0),
            'entry_date' => (string) ($row['entry_date'] ?? ''),
            'entry_number' => (string) ($row['entry_number'] ?? ''),
            'reference' => (string) ($row['reference'] ?? ''),
            'line_no' => (int) ($row['line_no'] ?? 0),
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'cost_center_code' => (string) ($row['cost_center_code'] ?? ''),
            'cost_center_name' => (string) ($row['cost_center_name'] ?? ''),
            'description' => app_normalize_whitespace((string) (($row['line_description'] ?? '') !== '' ? $row['line_description'] : ($row['entry_description'] ?? ''))),
            'debit' => $debit,
            'credit' => $credit,
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'source_label' => app_source_module_label($sourceType),
            'source_url' => $sourceUrl,
        ];
    }

    return [
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'rows' => $resultRows,
        'totals' => $totals,
    ];
}

function app_accounting_cost_center_report(int $companyId, string $fromDate, string $toDate, string $costCenterCode = ''): array
{
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    $costCenterCode = app_normalize_whitespace($costCenterCode);
    if ($companyId <= 0) {
        return [
            'from_date' => $fromDate,
            'to_date' => $toDate,
            'selected_cost_center' => $costCenterCode,
            'summary' => [],
            'lines' => [],
            'totals' => ['debit' => 0.0, 'credit' => 0.0, 'net' => 0.0],
        ];
    }

    $params = [
        'company_id_entries' => $companyId,
        'company_id_lines' => $companyId,
        'status' => 'posted',
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ];
    $costCenterFilterSql = '';
    if ($costCenterCode !== '') {
        $costCenterFilterSql = ' AND l.cost_center_code = :cost_center_code';
        $params['cost_center_code'] = $costCenterCode;
    }

    $pdo = app_pdo();
    $summaryStmt = $pdo->prepare(
        'SELECT
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             COALESCE(SUM(l.debit), 0) AS total_debit,
             COALESCE(SUM(l.credit), 0) AS total_credit,
             COUNT(*) AS lines_count
         FROM journal_entry_lines l
         INNER JOIN journal_entries j ON j.id = l.journal_entry_id
         INNER JOIN chart_of_accounts a
           ON a.company_id = l.company_id
          AND a.account_code = l.account_code
         LEFT JOIN cost_centers cc
           ON cc.company_id = l.company_id
          AND cc.center_code = l.cost_center_code
         WHERE j.company_id = :company_id_entries
           AND l.company_id = :company_id_lines
           AND j.status = :status
           AND j.entry_date >= :from_date
           AND j.entry_date <= :to_date
           AND l.cost_center_code <> \'\'
           AND a.account_type IN (\'revenue\', \'expense\')' . $costCenterFilterSql . '
         GROUP BY l.cost_center_code, cc.center_name
         ORDER BY l.cost_center_code ASC'
    );
    $summaryStmt->execute($params);
    $summaryRows = $summaryStmt->fetchAll() ?: [];

    $summary = [];
    foreach ($summaryRows as $row) {
        $debit = app_number($row['total_debit'] ?? 0);
        $credit = app_number($row['total_credit'] ?? 0);
        $summary[] = [
            'cost_center_code' => (string) ($row['cost_center_code'] ?? ''),
            'cost_center_name' => (string) ($row['cost_center_name'] ?? ''),
            'total_debit' => $debit,
            'total_credit' => $credit,
            'net' => $debit - $credit,
            'lines_count' => (int) ($row['lines_count'] ?? 0),
        ];
    }

    $linesStmt = $pdo->prepare(
        'SELECT
             j.id AS journal_entry_id,
             j.entry_date,
             j.entry_number,
             j.reference,
             j.source_type,
             j.source_id,
             l.line_no,
             l.account_code,
             COALESCE(a.account_name, l.account_code) AS account_name,
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             l.description AS line_description,
             j.description AS entry_description,
             l.debit,
             l.credit
         FROM journal_entry_lines l
         INNER JOIN journal_entries j ON j.id = l.journal_entry_id
         INNER JOIN chart_of_accounts a
           ON a.company_id = l.company_id
          AND a.account_code = l.account_code
         LEFT JOIN cost_centers cc
           ON cc.company_id = l.company_id
          AND cc.center_code = l.cost_center_code
         WHERE j.company_id = :company_id_entries
           AND l.company_id = :company_id_lines
           AND j.status = :status
           AND j.entry_date >= :from_date
           AND j.entry_date <= :to_date
           AND l.cost_center_code <> \'\'
           AND a.account_type IN (\'revenue\', \'expense\')' . $costCenterFilterSql . '
         ORDER BY l.cost_center_code ASC, j.entry_date ASC, j.id ASC, l.line_no ASC, l.id ASC'
    );
    $linesStmt->execute($params);
    $lineRows = $linesStmt->fetchAll() ?: [];

    $lines = [];
    $totals = ['debit' => 0.0, 'credit' => 0.0, 'net' => 0.0];
    foreach ($lineRows as $row) {
        $debit = app_number($row['debit'] ?? 0);
        $credit = app_number($row['credit'] ?? 0);
        $sourceType = app_normalize_whitespace((string) ($row['source_type'] ?? ''));
        $sourceId = (int) ($row['source_id'] ?? 0);
        $lines[] = [
            'journal_entry_id' => (int) ($row['journal_entry_id'] ?? 0),
            'entry_date' => (string) ($row['entry_date'] ?? ''),
            'entry_number' => (string) ($row['entry_number'] ?? ''),
            'reference' => (string) ($row['reference'] ?? ''),
            'line_no' => (int) ($row['line_no'] ?? 0),
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'cost_center_code' => (string) ($row['cost_center_code'] ?? ''),
            'cost_center_name' => (string) ($row['cost_center_name'] ?? ''),
            'description' => app_normalize_whitespace((string) (($row['line_description'] ?? '') !== '' ? $row['line_description'] : ($row['entry_description'] ?? ''))),
            'debit' => $debit,
            'credit' => $credit,
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'source_label' => app_source_module_label($sourceType),
            'source_url' => app_source_module_url($sourceType, $sourceId, $companyId),
        ];
        $totals['debit'] += $debit;
        $totals['credit'] += $credit;
    }
    $totals['net'] = $totals['debit'] - $totals['credit'];

    return [
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'selected_cost_center' => $costCenterCode,
        'summary' => $summary,
        'lines' => $lines,
        'totals' => $totals,
    ];
}

function app_journal_entry_lines(int $companyId, int $journalEntryId): array
{
    if ($companyId <= 0 || $journalEntryId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT
             l.line_no,
             l.account_code,
             COALESCE(c.account_name, l.account_code) AS account_name,
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             l.description,
             l.debit,
             l.credit
         FROM journal_entry_lines l
         LEFT JOIN chart_of_accounts c
           ON c.company_id = l.company_id
          AND c.account_code = l.account_code
         LEFT JOIN cost_centers cc
           ON cc.company_id = l.company_id
          AND cc.center_code = l.cost_center_code
         WHERE l.company_id = :company_id
           AND l.journal_entry_id = :journal_entry_id
         ORDER BY l.line_no ASC, l.id ASC'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'journal_entry_id' => $journalEntryId,
    ]);
    return $stmt->fetchAll() ?: [];
}

function app_journal_complete_lines(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT
             j.id AS journal_entry_id,
             j.entry_number,
             j.entry_date,
             j.reference,
             j.description AS entry_description,
             j.status,
             j.source_type,
             j.source_id,
             l.line_no,
             l.account_code,
             COALESCE(c.account_name, l.account_code) AS account_name,
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             l.description AS line_description,
             l.debit,
             l.credit
         FROM journal_entries j
         INNER JOIN journal_entry_lines l ON l.journal_entry_id = j.id
         LEFT JOIN chart_of_accounts c
           ON c.company_id = l.company_id
          AND c.account_code = l.account_code
         LEFT JOIN cost_centers cc
           ON cc.company_id = l.company_id
          AND cc.center_code = l.cost_center_code
         WHERE j.company_id = :company_id_entries
           AND l.company_id = :company_id_lines
         ORDER BY j.entry_date DESC, j.id DESC, l.line_no ASC, l.id ASC'
    );
    $stmt->execute([
        'company_id_entries' => $companyId,
        'company_id_lines' => $companyId,
    ]);
    $rows = $stmt->fetchAll() ?: [];

    $result = [];
    foreach ($rows as $row) {
        $sourceType = app_normalize_whitespace((string) ($row['source_type'] ?? ''));
        $sourceId = (int) ($row['source_id'] ?? 0);

        $result[] = [
            'journal_entry_id' => (int) ($row['journal_entry_id'] ?? 0),
            'entry_number' => (string) ($row['entry_number'] ?? ''),
            'entry_date' => (string) ($row['entry_date'] ?? ''),
            'reference' => (string) ($row['reference'] ?? ''),
            'status' => (string) ($row['status'] ?? 'draft'),
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'source_label' => app_source_module_label($sourceType),
            'source_url' => app_source_module_url($sourceType, $sourceId, $companyId),
            'line_no' => (int) ($row['line_no'] ?? 0),
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'cost_center_code' => (string) ($row['cost_center_code'] ?? ''),
            'cost_center_name' => (string) ($row['cost_center_name'] ?? ''),
            'description' => app_normalize_whitespace((string) (($row['line_description'] ?? '') !== '' ? $row['line_description'] : ($row['entry_description'] ?? ''))),
            'debit' => app_number($row['debit'] ?? 0),
            'credit' => app_number($row['credit'] ?? 0),
        ];
    }

    return $result;
}

function app_depreciation_run_lines(int $companyId, int $runId): array
{
    if ($companyId <= 0 || $runId <= 0) {
        return [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT
             l.id,
             l.run_id,
             l.asset_id,
             l.asset_code,
             l.asset_name,
             l.cost_center_code,
             COALESCE(cc.center_name, l.cost_center_code) AS cost_center_name,
             l.depreciation_amount,
             l.previous_accumulated,
             l.new_accumulated
         FROM depreciation_run_lines l
         LEFT JOIN cost_centers cc
           ON cc.company_id = l.company_id
          AND cc.center_code = l.cost_center_code
         WHERE l.company_id = :company_id
           AND l.run_id = :run_id
         ORDER BY l.asset_code ASC, l.id ASC'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'run_id' => $runId,
    ]);
    return $stmt->fetchAll() ?: [];
}

function app_company_fiscal_start_date(int $companyId, string $asOfDate): string
{
    $asOfDate = app_sanitize_date($asOfDate, date('Y-m-d'));
    $company = app_company_by_id($companyId);
    $fiscal = app_normalize_whitespace((string) ($company['fiscal_year_start'] ?? '01-01'));
    if (!preg_match('/^\d{2}-\d{2}$/', $fiscal)) {
        $fiscal = '01-01';
    }

    $year = (int) date('Y', strtotime($asOfDate));
    $candidate = $year . '-' . $fiscal;
    if (strtotime($asOfDate) < strtotime($candidate)) {
        $candidate = ($year - 1) . '-' . $fiscal;
    }
    return app_sanitize_date($candidate, date('Y-01-01'));
}

function app_financial_statement_display_amount(string $accountType, float $signedAmount): float
{
    $accountType = strtolower(app_normalize_whitespace($accountType));
    return in_array($accountType, ['liability', 'equity', 'revenue'], true)
        ? -$signedAmount
        : $signedAmount;
}

function app_financial_statement_empty_sections(string $statementType): array
{
    $statementType = app_normalize_whitespace($statementType);
    $sections = [];
    foreach (app_financial_statement_main_sections() as $sectionCode => $section) {
        if ((string) ($section['statement_type'] ?? '') !== $statementType) {
            continue;
        }
        $sections[$sectionCode] = [
            'code' => $sectionCode,
            'label' => app_financial_statement_section_label($sectionCode),
            'total_label' => app_financial_statement_section_label($sectionCode, true),
            'sort_order' => (int) ($section['sort_order'] ?? 0),
            'presentation_sign' => (int) ($section['presentation_sign'] ?? 1),
            'groups' => [],
            'total' => 0.0,
            'display_total' => 0.0,
        ];
    }
    uasort(
        $sections,
        static fn (array $a, array $b): int => ((int) ($a['sort_order'] ?? 0)) <=> ((int) ($b['sort_order'] ?? 0))
    );
    return $sections;
}

function app_financial_statement_grouped_sections(int $companyId, string $statementType, array $trialRows, string $amountColumn): array
{
    $statementType = app_normalize_whitespace($statementType);
    $sections = app_financial_statement_empty_sections($statementType);
    if ($companyId <= 0 || !in_array($statementType, ['balance_sheet', 'income_statement'], true)) {
        return ['sections' => $sections, 'enabled' => false, 'has_unmapped' => false];
    }

    $pdo = app_pdo();
    $groupStmt = $pdo->prepare(
        'SELECT group_code, group_name, statement_type, main_section, sort_order, is_active
         FROM financial_statement_groups
         WHERE company_id = :company_id
           AND statement_type = :statement_type
           AND is_active = 1
         ORDER BY main_section ASC, sort_order ASC, group_code ASC'
    );
    $groupStmt->execute([
        'company_id' => $companyId,
        'statement_type' => $statementType,
    ]);
    $configuredGroups = $groupStmt->fetchAll() ?: [];

    $groupsByCode = [];
    foreach ($configuredGroups as $group) {
        $code = app_normalize_whitespace((string) ($group['group_code'] ?? ''));
        $sectionCode = app_normalize_whitespace((string) ($group['main_section'] ?? ''));
        if ($code === '' || !isset($sections[$sectionCode])) {
            continue;
        }
        $groupsByCode[$code] = $group;
        $sections[$sectionCode]['groups'][$code] = [
            'code' => $code,
            'name' => app_normalize_whitespace((string) ($group['group_name'] ?? $code)),
            'sort_order' => (int) ($group['sort_order'] ?? 0),
            'amount' => 0.0,
            'display_amount' => 0.0,
            'accounts' => [],
            'is_unmapped' => false,
        ];
    }

    $hasMappedAccount = false;
    $hasUnmapped = false;
    foreach ($trialRows as $row) {
        $accountType = strtolower(app_normalize_whitespace((string) ($row['account_type'] ?? '')));
        if ($statementType === 'balance_sheet' && !in_array($accountType, ['asset', 'liability', 'equity'], true)) {
            continue;
        }
        if ($statementType === 'income_statement' && !in_array($accountType, ['revenue', 'expense'], true)) {
            continue;
        }

        $amount = app_financial_statement_display_amount($accountType, app_number($row[$amountColumn] ?? 0));
        if (abs($amount) < 0.0001) {
            continue;
        }

        $groupCode = app_normalize_whitespace((string) ($row['financial_statement_group_code'] ?? ''));
        $group = $groupCode !== '' ? ($groupsByCode[$groupCode] ?? null) : null;
        if ($group) {
            $sectionCode = app_normalize_whitespace((string) ($group['main_section'] ?? ''));
            $hasMappedAccount = true;
        } else {
            $sectionCode = app_financial_statement_section_for_account_type($accountType, $statementType);
            if ($sectionCode === '' || !isset($sections[$sectionCode])) {
                continue;
            }
            $groupCode = '__unmapped_' . $sectionCode;
            $hasUnmapped = true;
            if (!isset($sections[$sectionCode]['groups'][$groupCode])) {
                $sections[$sectionCode]['groups'][$groupCode] = [
                    'code' => '',
                    'name' => app_t('Unmapped Accounts'),
                    'sort_order' => 999999,
                    'amount' => 0.0,
                    'display_amount' => 0.0,
                    'accounts' => [],
                    'is_unmapped' => true,
                ];
            }
        }

        $sections[$sectionCode]['groups'][$groupCode]['amount'] += $amount;
        $sections[$sectionCode]['groups'][$groupCode]['accounts'][] = [
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'account_type' => $accountType,
            'amount' => $amount,
        ];
    }

    foreach ($sections as $sectionCode => $section) {
        uasort(
            $sections[$sectionCode]['groups'],
            static function (array $a, array $b): int {
                $order = ((int) ($a['sort_order'] ?? 0)) <=> ((int) ($b['sort_order'] ?? 0));
                if ($order !== 0) {
                    return $order;
                }
                return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
            }
        );

        $sectionTotal = 0.0;
        $presentationSign = (int) ($section['presentation_sign'] ?? 1);
        foreach ($sections[$sectionCode]['groups'] as $groupCode => $group) {
            $amount = round(app_number($group['amount'] ?? 0), 2);
            $sections[$sectionCode]['groups'][$groupCode]['amount'] = $amount;
            $sections[$sectionCode]['groups'][$groupCode]['display_amount'] = round($amount * $presentationSign, 2);
            $sectionTotal += $amount;
        }
        $sections[$sectionCode]['total'] = round($sectionTotal, 2);
        $sections[$sectionCode]['display_total'] = round($sectionTotal * $presentationSign, 2);
    }

    return [
        'sections' => $sections,
        'enabled' => (bool) ($configuredGroups || $hasMappedAccount),
        'has_unmapped' => $hasUnmapped,
    ];
}

function app_accounting_income_statement(int $companyId, string $fromDate, string $toDate): array
{
    $trial = app_accounting_trial_balance($companyId, $fromDate, $toDate, false);
    $revenues = [];
    $expenses = [];
    $totalRevenue = 0.0;
    $totalExpense = 0.0;

    foreach ((array) ($trial['rows'] ?? []) as $row) {
        $type = strtolower((string) ($row['account_type'] ?? ''));
        $signedAmount = app_number($row['period_signed'] ?? 0);
        if ($type === 'revenue') {
            $amount = -$signedAmount;
            $revenues[] = [
                'account_code' => (string) ($row['account_code'] ?? ''),
                'account_name' => (string) ($row['account_name'] ?? ''),
                'amount' => $amount,
            ];
            $totalRevenue += $amount;
        } elseif ($type === 'expense') {
            $amount = $signedAmount;
            $expenses[] = [
                'account_code' => (string) ($row['account_code'] ?? ''),
                'account_name' => (string) ($row['account_name'] ?? ''),
                'amount' => $amount,
            ];
            $totalExpense += $amount;
        }
    }

    $grouped = app_financial_statement_grouped_sections($companyId, 'income_statement', (array) ($trial['rows'] ?? []), 'period_signed');

    return [
        'from_date' => (string) ($trial['from_date'] ?? ''),
        'to_date' => (string) ($trial['to_date'] ?? ''),
        'revenues' => $revenues,
        'expenses' => $expenses,
        'total_revenue' => $totalRevenue,
        'total_expense' => $totalExpense,
        'net_income' => $totalRevenue - $totalExpense,
        'sections' => $grouped['sections'] ?? [],
        'grouped_enabled' => (bool) ($grouped['enabled'] ?? false),
        'has_unmapped' => (bool) ($grouped['has_unmapped'] ?? false),
    ];
}

function app_budget_summary_for_expense_account(int $companyId, string $accountCode, string $asOfDate = ''): array
{
    $accountCode = app_normalize_whitespace($accountCode);
    $asOfDate = app_sanitize_date($asOfDate !== '' ? $asOfDate : date('Y-m-d'), date('Y-m-d'));
    $fiscalYear = (string) date('Y', strtotime($asOfDate));
    $empty = [
        'account_code' => $accountCode,
        'fiscal_year' => $fiscalYear,
        'budget_amount' => 0.0,
        'spent_amount' => 0.0,
        'remaining_amount' => 0.0,
        'usage_percent' => 0.0,
    ];

    if ($companyId <= 0 || $accountCode === '') {
        return $empty;
    }

    $pdo = app_pdo();
    $budgetStmt = $pdo->prepare(
        'SELECT COALESCE(SUM(budget_amount), 0)
         FROM budgets
         WHERE company_id = :company_id
           AND account_code = :account_code
           AND fiscal_year = :fiscal_year'
    );
    $budgetStmt->execute([
        'company_id' => $companyId,
        'account_code' => $accountCode,
        'fiscal_year' => $fiscalYear,
    ]);
    $budgetAmount = max(0, app_number($budgetStmt->fetchColumn()));

    $spentStmt = $pdo->prepare(
        'SELECT COALESCE(SUM(GREATEST(grand_total - tax_total, 0)), 0)
         FROM purchase_bills
         WHERE company_id = :company_id
           AND expense_account_code = :account_code
           AND status NOT IN (:draft_status, :cancelled_status)
           AND bill_date >= :from_date
           AND bill_date <= :to_date'
    );
    $spentStmt->execute([
        'company_id' => $companyId,
        'account_code' => $accountCode,
        'draft_status' => 'draft',
        'cancelled_status' => 'cancelled',
        'from_date' => $fiscalYear . '-01-01',
        'to_date' => $fiscalYear . '-12-31',
    ]);
    $spentAmount = max(0, app_number($spentStmt->fetchColumn()));
    $remainingAmount = round($budgetAmount - $spentAmount, 2);
    $usagePercent = $budgetAmount > 0 ? (($spentAmount / $budgetAmount) * 100) : 0.0;

    return [
        'account_code' => $accountCode,
        'fiscal_year' => $fiscalYear,
        'budget_amount' => round($budgetAmount, 2),
        'spent_amount' => round($spentAmount, 2),
        'remaining_amount' => $remainingAmount,
        'usage_percent' => round($usagePercent, 2),
    ];
}

function app_accounting_balance_sheet(int $companyId, string $asOfDate): array
{
    $asOfDate = app_sanitize_date($asOfDate, date('Y-m-d'));
    $trial = app_accounting_trial_balance($companyId, '1900-01-01', $asOfDate, false);
    $balanceTrialRows = (array) ($trial['rows'] ?? []);

    $assets = [];
    $liabilities = [];
    $equity = [];
    $totalAssets = 0.0;
    $totalLiabilities = 0.0;
    $totalEquity = 0.0;

    foreach ((array) ($trial['rows'] ?? []) as $row) {
        $type = strtolower((string) ($row['account_type'] ?? ''));
        $closingSigned = app_number($row['closing_signed'] ?? 0);
        if (abs($closingSigned) < 0.0001) {
            continue;
        }

        $displayAmount = in_array($type, ['liability', 'equity'], true)
            ? -$closingSigned
            : $closingSigned;

        $item = [
            'account_code' => (string) ($row['account_code'] ?? ''),
            'account_name' => (string) ($row['account_name'] ?? ''),
            'amount' => $displayAmount,
            'signed_amount' => $closingSigned,
        ];

        if ($type === 'asset') {
            $assets[] = $item;
            $totalAssets += $displayAmount;
        } elseif ($type === 'liability') {
            $liabilities[] = $item;
            $totalLiabilities += $displayAmount;
        } elseif ($type === 'equity') {
            $equity[] = $item;
            $totalEquity += $displayAmount;
        }
    }

    $fiscalStart = app_company_fiscal_start_date($companyId, $asOfDate);
    $income = app_accounting_income_statement($companyId, $fiscalStart, $asOfDate);
    $currentYearEarnings = app_number($income['net_income'] ?? 0);
    if (abs($currentYearEarnings) > 0.0001) {
        $equity[] = [
            'account_code' => 'CYE',
            'account_name' => 'Current Year Earnings',
            'amount' => $currentYearEarnings,
        ];
        $balanceTrialRows[] = [
            'account_code' => 'CYE',
            'account_name' => 'Current Year Earnings',
            'account_type' => 'equity',
            'closing_signed' => -$currentYearEarnings,
            'financial_statement_group_code' => '',
        ];
        $totalEquity += $currentYearEarnings;
    }

    $grouped = app_financial_statement_grouped_sections($companyId, 'balance_sheet', $balanceTrialRows, 'closing_signed');

    return [
        'as_of_date' => $asOfDate,
        'assets' => $assets,
        'liabilities' => $liabilities,
        'equity' => $equity,
        'total_assets' => $totalAssets,
        'total_liabilities' => $totalLiabilities,
        'total_equity' => $totalEquity,
        'equity_and_liabilities' => $totalLiabilities + $totalEquity,
        'fiscal_start' => $fiscalStart,
        'sections' => $grouped['sections'] ?? [],
        'grouped_enabled' => (bool) ($grouped['enabled'] ?? false),
        'has_unmapped' => (bool) ($grouped['has_unmapped'] ?? false),
    ];
}

function app_ecl_normal_cdf(float $x): float
{
    $sign = $x < 0 ? -1.0 : 1.0;
    $x = abs($x) / sqrt(2.0);
    $t = 1.0 / (1.0 + 0.3275911 * $x);
    $a1 = 0.254829592;
    $a2 = -0.284496736;
    $a3 = 1.421413741;
    $a4 = -1.453152027;
    $a5 = 1.061405429;
    $erf = 1.0 - (((((($a5 * $t) + $a4) * $t + $a3) * $t + $a2) * $t + $a1) * $t * exp(-$x * $x));
    return max(0.0, min(1.0, 0.5 * (1.0 + $sign * $erf)));
}

function app_ecl_normal_inv(float $p): float
{
    $p = max(0.000000001, min(0.999999999, $p));
    $a = [-39.69683028665376, 220.9460984245205, -275.9285104469687, 138.3577518672690, -30.66479806614716, 2.506628277459239];
    $b = [-54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572];
    $c = [-0.007784894002430293, -0.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
    $d = [0.007784695709041462, 0.3224671290700398, 2.445134137142996, 3.754408661907416];
    $plow = 0.02425;
    $phigh = 1.0 - $plow;

    if ($p < $plow) {
        $q = sqrt(-2.0 * log($p));
        return (((((($c[0] * $q + $c[1]) * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) /
            (((($d[0] * $q + $d[1]) * $q + $d[2]) * $q + $d[3]) * $q + 1.0));
    }
    if ($p > $phigh) {
        $q = sqrt(-2.0 * log(1.0 - $p));
        return -(((((($c[0] * $q + $c[1]) * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) /
            (((($d[0] * $q + $d[1]) * $q + $d[2]) * $q + $d[3]) * $q + 1.0));
    }

    $q = $p - 0.5;
    $r = $q * $q;
    return (((((($a[0] * $r + $a[1]) * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $q) /
        (((((($b[0] * $r + $b[1]) * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + 1.0));
}

function app_ecl_split_codes(string $codes): array
{
    $parts = preg_split('/[\s,;]+/', $codes) ?: [];
    $result = [];
    foreach ($parts as $part) {
        $code = app_normalize_whitespace((string) $part);
        if ($code !== '') {
            $result[$code] = $code;
        }
    }
    return array_values($result);
}

function app_ecl_trial_rows_by_code(int $companyId, string $asOfDate): array
{
    $trial = app_accounting_trial_balance($companyId, '1900-01-01', $asOfDate, false);
    $rowsByCode = [];
    foreach ((array) ($trial['rows'] ?? []) as $row) {
        $code = app_normalize_whitespace((string) ($row['account_code'] ?? ''));
        if ($code !== '') {
            $rowsByCode[$code] = $row;
        }
    }
    return $rowsByCode;
}

function app_ecl_expand_account_codes(int $companyId, array $codes): array
{
    $selected = [];
    foreach ($codes as $code) {
        $code = app_normalize_whitespace((string) $code);
        if ($code !== '') {
            $selected[$code] = true;
        }
    }
    if (!$selected) {
        return [];
    }

    $childrenByParent = [];
    foreach (app_chart_accounts($companyId, null, false) as $account) {
        $code = app_normalize_whitespace((string) ($account['account_code'] ?? ''));
        $parent = app_normalize_whitespace((string) ($account['parent_code'] ?? ''));
        if ($code !== '' && $parent !== '') {
            $childrenByParent[$parent][] = $code;
        }
    }

    $queue = array_keys($selected);
    while ($queue) {
        $code = array_shift($queue);
        foreach (($childrenByParent[$code] ?? []) as $childCode) {
            if (!isset($selected[$childCode])) {
                $selected[$childCode] = true;
                $queue[] = $childCode;
            }
        }
    }

    return array_keys($selected);
}

function app_ecl_account_codes_for_group(int $companyId, string $groupCode): array
{
    $groupCode = app_normalize_whitespace($groupCode);
    if ($companyId <= 0 || $groupCode === '') {
        return [];
    }

    $stmt = app_pdo()->prepare(
        'SELECT account_code
         FROM chart_of_accounts
         WHERE company_id = :company_id
           AND financial_statement_group_code = :group_code
           AND is_active = 1
         ORDER BY account_code ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'group_code' => $groupCode]);
    $codes = [];
    foreach (($stmt->fetchAll() ?: []) as $row) {
        $code = app_normalize_whitespace((string) ($row['account_code'] ?? ''));
        if ($code !== '') {
            $codes[] = $code;
        }
    }
    return $codes;
}

function app_ecl_amount_from_account_codes(int $companyId, array $trialRowsByCode, array $codes, string $groupCode = ''): float
{
    $allCodes = $codes;
    if ($groupCode !== '') {
        $allCodes = array_merge($allCodes, app_ecl_account_codes_for_group($companyId, $groupCode));
    }
    $expandedCodes = app_ecl_expand_account_codes($companyId, $allCodes);
    if (!$expandedCodes) {
        return 0.0;
    }

    $amount = 0.0;
    foreach ($expandedCodes as $code) {
        $row = $trialRowsByCode[$code] ?? null;
        if (!$row) {
            continue;
        }
        $amount += max(0.0, app_number($row['closing_signed'] ?? 0));
    }
    return round($amount, 2);
}

function app_ecl_country_profile(int $companyId, string $countryCode): ?array
{
    $countryCode = app_normalize_whitespace($countryCode);
    if ($companyId <= 0 || $countryCode === '') {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM ecl_countries
         WHERE company_id = :company_id
           AND country_code = :country_code
           AND is_active = 1
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'country_code' => $countryCode]);
    $profile = $stmt->fetch() ?: null;
    return is_array($profile) ? $profile : null;
}

function app_ecl_default_country_code(int $companyId): string
{
    $options = app_module_lookup_options('ecl_country_code', $companyId);
    if (array_key_exists('SAUDI_ARABIA', $options)) {
        return 'SAUDI_ARABIA';
    }

    $firstCode = array_key_first($options);
    return is_string($firstCode) ? $firstCode : 'SAUDI_ARABIA';
}

function app_ecl_rating_pd(int $companyId, string $agency, string $rating): ?float
{
    $agency = strtolower(app_normalize_whitespace($agency));
    $rating = app_normalize_whitespace($rating);
    if ($companyId <= 0 || $agency === '' || $rating === '') {
        return null;
    }

    $stmt = app_pdo()->prepare(
        'SELECT one_year_pd_percent
         FROM ecl_default_rates
         WHERE company_id = :company_id
           AND agency = :agency
           AND rating_code = :rating
           AND is_active = 1
         LIMIT 1'
    );
    $stmt->execute(['company_id' => $companyId, 'agency' => $agency, 'rating' => $rating]);
    $value = $stmt->fetchColumn();
    if ($value === false) {
        return null;
    }
    return max(0.0, min(100.0, app_number($value))) / 100.0;
}

function app_ecl_country_loss_model(int $companyId, string $countryCode, float $lgdPercent): array
{
    $profile = app_ecl_country_profile($companyId, $countryCode);
    if (!$profile) {
        return [
            'country' => null,
            'ttc_pd' => 0.0,
            'pit_pd' => 0.0,
            'loss_rate' => 0.0,
            'ratings' => [],
            'message' => 'Country is not configured.',
        ];
    }

    $ratings = [
        'sp' => app_normalize_whitespace((string) ($profile['sp_rating'] ?? '')),
        'moodys' => app_normalize_whitespace((string) ($profile['moodys_rating'] ?? '')),
        'fitch' => app_normalize_whitespace((string) ($profile['fitch_rating'] ?? '')),
    ];
    $ratingPds = [];
    foreach ($ratings as $agency => $rating) {
        $ratingPds[$agency] = app_ecl_rating_pd($companyId, $agency, $rating);
    }
    $availablePds = array_values(array_filter($ratingPds, static fn (?float $pd): bool => $pd !== null));
    $ttcPd = $availablePds ? max($availablePds) : 0.0;
    if ($ttcPd <= 0.0) {
        return [
            'country' => $profile,
            'ttc_pd' => 0.0,
            'pit_pd' => 0.0,
            'loss_rate' => 0.0,
            'ratings' => $ratingPds,
            'message' => 'No rating PD was found for this country.',
        ];
    }

    $denominator = 1.0 - exp(-50.0);
    $assetCorrelation = 0.12 * (1.0 - exp(-50.0 * $ttcPd)) / $denominator
        + 0.24 * (1.0 - ((1.0 - exp(-50.0 * $ttcPd)) / $denominator));
    $assetCorrelation = max(0.000001, min(0.999999, $assetCorrelation));
    $invPd = app_ecl_normal_inv($ttcPd);
    $scenarioPit = static function (float $factor) use ($invPd, $assetCorrelation): float {
        return app_ecl_normal_cdf(($invPd - sqrt($assetCorrelation) * $factor) / sqrt(1.0 - $assetCorrelation));
    };

    $baseWeight = max(0.0, app_number($profile['base_weight_percent'] ?? 0)) / 100.0;
    $bestWeight = max(0.0, app_number($profile['best_weight_percent'] ?? 0)) / 100.0;
    $worstWeight = max(0.0, app_number($profile['worst_weight_percent'] ?? 0)) / 100.0;
    $totalWeight = $baseWeight + $bestWeight + $worstWeight;
    if ($totalWeight <= 0.0) {
        $baseWeight = 0.6;
        $bestWeight = 0.1;
        $worstWeight = 0.3;
        $totalWeight = 1.0;
    }

    $basePit = $scenarioPit(app_number($profile['base_scenario_factor'] ?? 0));
    $bestPit = $scenarioPit(app_number($profile['best_scenario_factor'] ?? 0));
    $worstPit = $scenarioPit(app_number($profile['worst_scenario_factor'] ?? 0));
    $pitPd = (($basePit * $baseWeight) + ($bestPit * $bestWeight) + ($worstPit * $worstWeight)) / $totalWeight;
    $lossRate = $pitPd * max(0.0, min(100.0, $lgdPercent)) / 100.0;

    return [
        'country' => $profile,
        'ttc_pd' => $ttcPd,
        'pit_pd' => $pitPd,
        'loss_rate' => $lossRate,
        'ratings' => $ratingPds,
        'asset_correlation' => $assetCorrelation,
        'scenario_pit' => ['base' => $basePit, 'best' => $bestPit, 'worst' => $worstPit],
        'message' => '',
    ];
}

function app_ecl_receivable_buckets(int $companyId): array
{
    if ($companyId <= 0) {
        return [];
    }
    $stmt = app_pdo()->prepare(
        'SELECT *
         FROM ecl_receivable_buckets
         WHERE company_id = :company_id
           AND is_active = 1
         ORDER BY sort_order ASC, min_days ASC, id ASC'
    );
    $stmt->execute(['company_id' => $companyId]);
    return $stmt->fetchAll() ?: [];
}

function app_ecl_receivable_bucket_for_days(array $buckets, int $days): ?array
{
    foreach ($buckets as $bucket) {
        $minDays = (int) ($bucket['min_days'] ?? 0);
        $maxDays = (int) ($bucket['max_days'] ?? 0);
        if ($days >= $minDays && ($maxDays <= 0 || $days <= $maxDays)) {
            return $bucket;
        }
    }
    return $buckets ? (array) end($buckets) : null;
}

function app_ecl_sales_receivables_analysis(int $companyId, string $asOfDate): array
{
    $asOfDate = app_sanitize_date($asOfDate, date('Y-m-d'));
    $buckets = app_ecl_receivable_buckets($companyId);
    $bucketRows = [];
    foreach ($buckets as $bucket) {
        $code = app_normalize_whitespace((string) ($bucket['bucket_code'] ?? ''));
        if ($code === '') {
            continue;
        }
        $bucketRows[$code] = [
            'bucket_code' => $code,
            'bucket_name' => (string) ($bucket['bucket_name'] ?? $code),
            'min_days' => (int) ($bucket['min_days'] ?? 0),
            'max_days' => (int) ($bucket['max_days'] ?? 0),
            'loss_rate' => max(0.0, app_number($bucket['loss_rate_percent'] ?? 0)) / 100.0,
            'gross' => 0.0,
            'allowance' => 0.0,
            'invoice_count' => 0,
        ];
    }

    $stmt = app_pdo()->prepare(
        'SELECT
             s.id,
             s.invoice_number,
             s.invoice_date,
             s.grand_total,
             s.inline_paid_amount,
             COALESCE(SUM(CASE WHEN p.payment_date <= :payment_date THEN p.amount ELSE 0 END), 0) AS paid_from_payments
         FROM sales_invoices s
         LEFT JOIN payments p
           ON p.company_id = s.company_id
          AND p.sales_invoice_id = s.id
         WHERE s.company_id = :company_id
           AND s.invoice_date <= :as_of_date
           AND s.status NOT IN (\'draft\', \'cancelled\')
         GROUP BY s.id, s.invoice_number, s.invoice_date, s.grand_total, s.inline_paid_amount
         ORDER BY s.invoice_date ASC, s.id ASC'
    );
    $stmt->execute(['company_id' => $companyId, 'as_of_date' => $asOfDate, 'payment_date' => $asOfDate]);

    $gross = 0.0;
    $allowance = 0.0;
    foreach (($stmt->fetchAll() ?: []) as $invoice) {
        $invoiceTotal = max(0.0, app_number($invoice['grand_total'] ?? 0));
        $paid = max(0.0, app_number($invoice['inline_paid_amount'] ?? 0)) + max(0.0, app_number($invoice['paid_from_payments'] ?? 0));
        $outstanding = round(max(0.0, $invoiceTotal - $paid), 2);
        if ($outstanding <= 0.0001) {
            continue;
        }

        $invoiceDate = app_sanitize_date((string) ($invoice['invoice_date'] ?? $asOfDate), $asOfDate);
        $days = max(0, (int) floor((strtotime($asOfDate) - strtotime($invoiceDate)) / 86400));
        $bucket = app_ecl_receivable_bucket_for_days($buckets, $days);
        $bucketCode = app_normalize_whitespace((string) ($bucket['bucket_code'] ?? ''));
        if ($bucketCode === '') {
            continue;
        }
        if (!isset($bucketRows[$bucketCode])) {
            $bucketRows[$bucketCode] = [
                'bucket_code' => $bucketCode,
                'bucket_name' => (string) ($bucket['bucket_name'] ?? $bucketCode),
                'min_days' => (int) ($bucket['min_days'] ?? 0),
                'max_days' => (int) ($bucket['max_days'] ?? 0),
                'loss_rate' => max(0.0, app_number($bucket['loss_rate_percent'] ?? 0)) / 100.0,
                'gross' => 0.0,
                'allowance' => 0.0,
                'invoice_count' => 0,
            ];
        }
        $lossRate = app_number($bucketRows[$bucketCode]['loss_rate'] ?? 0);
        $lineAllowance = round($outstanding * $lossRate, 2);
        $bucketRows[$bucketCode]['gross'] += $outstanding;
        $bucketRows[$bucketCode]['allowance'] += $lineAllowance;
        $bucketRows[$bucketCode]['invoice_count']++;
        $gross += $outstanding;
        $allowance += $lineAllowance;
    }

    foreach ($bucketRows as $code => $row) {
        $bucketRows[$code]['gross'] = round(app_number($row['gross'] ?? 0), 2);
        $bucketRows[$code]['allowance'] = round(app_number($row['allowance'] ?? 0), 2);
    }

    return [
        'gross' => round($gross, 2),
        'allowance' => round($allowance, 2),
        'loss_rate' => $gross > 0 ? ($allowance / $gross) : 0.0,
        'buckets' => array_values($bucketRows),
    ];
}

function app_accounting_ecl_report(int $companyId, string $asOfDate, string $countryCode): array
{
    $asOfDate = app_sanitize_date($asOfDate, date('Y-m-d'));
    $countryCode = app_normalize_whitespace($countryCode);
    if ($companyId <= 0) {
        return ['as_of_date' => $asOfDate, 'country' => null, 'rows' => [], 'totals' => ['gross' => 0.0, 'allowance' => 0.0, 'loss_rate' => 0.0]];
    }

    if ($countryCode === '') {
        $countryCode = app_ecl_default_country_code($companyId);
    }
    $trialRowsByCode = app_ecl_trial_rows_by_code($companyId, $asOfDate);
    $pdo = app_pdo();
    $categoryStmt = $pdo->prepare(
        'SELECT *
         FROM ecl_report_categories
         WHERE company_id = :company_id
           AND is_active = 1
         ORDER BY sort_order ASC, category_code ASC'
    );
    $categoryStmt->execute(['company_id' => $companyId]);
    $categories = $categoryStmt->fetchAll() ?: [];

    $bankCodesStmt = $pdo->prepare(
        'SELECT gl_account_code, current_balance
         FROM bank_accounts
         WHERE company_id = :company_id
           AND is_active = 1
           AND bank_name <> \'Cash\''
    );
    $bankCodesStmt->execute(['company_id' => $companyId]);
    $bankAccountRows = $bankCodesStmt->fetchAll() ?: [];

    $receivableAnalysis = null;
    $rows = [];
    $totalGross = 0.0;
    $totalAllowance = 0.0;

    foreach ($categories as $category) {
        $sourceType = app_normalize_whitespace((string) ($category['source_type'] ?? 'account_balances'));
        $lossModel = app_normalize_whitespace((string) ($category['loss_model'] ?? 'country_12m'));
        $categoryCountry = app_normalize_whitespace((string) ($category['country_code'] ?? ''));
        if ($categoryCountry === '') {
            $categoryCountry = $countryCode;
        }
        $accountCodes = app_ecl_split_codes((string) ($category['account_codes'] ?? ''));
        $groupCode = app_normalize_whitespace((string) ($category['financial_statement_group_code'] ?? ''));

        $gross = 0.0;
        $details = [];
        if ($sourceType === 'bank_accounts') {
            if (!$accountCodes && $groupCode === '') {
                $bankCodes = [];
                $fallback = 0.0;
                foreach ($bankAccountRows as $bankRow) {
                    $bankCode = app_normalize_whitespace((string) ($bankRow['gl_account_code'] ?? ''));
                    if ($bankCode !== '') {
                        $bankCodes[] = $bankCode;
                    }
                    $fallback += max(0.0, app_number($bankRow['current_balance'] ?? 0));
                }
                $gross = app_ecl_amount_from_account_codes($companyId, $trialRowsByCode, $bankCodes);
                if ($gross <= 0.0001) {
                    $gross = round($fallback, 2);
                }
            } else {
                $gross = app_ecl_amount_from_account_codes($companyId, $trialRowsByCode, $accountCodes, $groupCode);
            }
        } elseif ($sourceType === 'sales_receivables') {
            if ($receivableAnalysis === null) {
                $receivableAnalysis = app_ecl_sales_receivables_analysis($companyId, $asOfDate);
            }
            $gross = app_number($receivableAnalysis['gross'] ?? 0);
            $details['receivable_buckets'] = (array) ($receivableAnalysis['buckets'] ?? []);
        } elseif ($sourceType === 'manual_amount') {
            $gross = max(0.0, app_number($category['manual_amount'] ?? 0));
        } else {
            $gross = app_ecl_amount_from_account_codes($companyId, $trialRowsByCode, $accountCodes, $groupCode);
        }

        $rateOverride = max(0.0, app_number($category['rate_override_percent'] ?? 0)) / 100.0;
        if ($rateOverride > 0.0) {
            $lossRate = $rateOverride;
            $allowance = round($gross * $lossRate, 2);
            $rateMeta = ['message' => 'Manual rate override'];
        } elseif ($lossModel === 'receivable_aging' && $sourceType === 'sales_receivables') {
            if ($receivableAnalysis === null) {
                $receivableAnalysis = app_ecl_sales_receivables_analysis($companyId, $asOfDate);
            }
            $lossRate = app_number($receivableAnalysis['loss_rate'] ?? 0);
            $allowance = round(app_number($receivableAnalysis['allowance'] ?? 0), 2);
            $rateMeta = ['message' => 'Aging bucket model'];
        } else {
            $rateMeta = app_ecl_country_loss_model($companyId, $categoryCountry, app_number($category['lgd_percent'] ?? 45));
            $lossRate = app_number($rateMeta['loss_rate'] ?? 0);
            $allowance = round($gross * $lossRate, 2);
        }

        $rows[] = [
            'category_code' => (string) ($category['category_code'] ?? ''),
            'category_name' => (string) ($category['category_name'] ?? ''),
            'source_type' => $sourceType,
            'loss_model' => $lossModel,
            'country_code' => $categoryCountry,
            'gross' => round($gross, 2),
            'loss_rate' => $gross > 0 ? $lossRate : 0.0,
            'allowance' => $gross > 0 ? $allowance : 0.0,
            'rate_meta' => $rateMeta,
            'details' => $details,
        ];
        $totalGross += round($gross, 2);
        $totalAllowance += $gross > 0 ? $allowance : 0.0;
    }

    return [
        'as_of_date' => $asOfDate,
        'country' => app_ecl_country_profile($companyId, $countryCode),
        'country_code' => $countryCode,
        'rows' => $rows,
        'totals' => [
            'gross' => round($totalGross, 2),
            'allowance' => round($totalAllowance, 2),
            'loss_rate' => $totalGross > 0 ? ($totalAllowance / $totalGross) : 0.0,
        ],
    ];
}

function app_accounting_cash_flow_summary(int $companyId, string $fromDate, string $toDate): array
{
    $fromDate = app_sanitize_date($fromDate, date('Y-01-01'));
    $toDate = app_sanitize_date($toDate, date('Y-m-d'));
    if ($fromDate > $toDate) {
        [$fromDate, $toDate] = [$toDate, $fromDate];
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT direction, payment_method, COALESCE(SUM(amount), 0) AS total_amount
         FROM payments
         WHERE company_id = :company_id
           AND payment_date >= :from_date
           AND payment_date <= :to_date
         GROUP BY direction, payment_method
         ORDER BY direction ASC, payment_method ASC'
    );
    $stmt->execute([
        'company_id' => $companyId,
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ]);
    $rows = $stmt->fetchAll() ?: [];

    $inTotal = 0.0;
    $outTotal = 0.0;
    $breakdown = [];
    foreach ($rows as $row) {
        $direction = strtolower((string) ($row['direction'] ?? ''));
        $method = app_normalize_whitespace((string) ($row['payment_method'] ?? ''));
        $amount = max(0, app_number($row['total_amount'] ?? 0));
        if ($direction === 'out') {
            $outTotal += $amount;
        } else {
            $inTotal += $amount;
        }
        $breakdown[] = [
            'direction' => $direction,
            'payment_method' => $method,
            'amount' => $amount,
        ];
    }

    $posStmt = $pdo->prepare(
        "SELECT p.payment_method, COALESCE(SUM(p.amount), 0) AS total_amount
         FROM pos_payments p
         INNER JOIN pos_orders o
                 ON o.company_id = p.company_id
                AND o.id = p.order_id
         WHERE p.company_id = :company_id
           AND DATE(o.order_date) >= :from_date
           AND DATE(o.order_date) <= :to_date
           AND o.status <> 'cancelled'
         GROUP BY p.payment_method
         ORDER BY p.payment_method ASC"
    );
    $posStmt->execute([
        'company_id' => $companyId,
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ]);
    foreach (($posStmt->fetchAll() ?: []) as $row) {
        $method = 'POS ' . app_normalize_whitespace((string) ($row['payment_method'] ?? 'cash'));
        $amount = round(app_number($row['total_amount'] ?? 0), 2);
        if (abs($amount) < 0.01) {
            continue;
        }
        if ($amount >= 0) {
            $inTotal += $amount;
            $direction = 'in';
        } else {
            $outTotal += abs($amount);
            $direction = 'out';
        }
        $breakdown[] = [
            'direction' => $direction,
            'payment_method' => $method,
            'amount' => abs($amount),
        ];
    }

    $posCashStmt = $pdo->prepare(
        "SELECT movement_type, COALESCE(SUM(amount), 0) AS total_amount
         FROM pos_cash_movements
         WHERE company_id = :company_id
           AND DATE(movement_date) >= :from_date
           AND DATE(movement_date) <= :to_date
           AND status <> 'cancelled'
         GROUP BY movement_type"
    );
    $posCashStmt->execute([
        'company_id' => $companyId,
        'from_date' => $fromDate,
        'to_date' => $toDate,
    ]);
    foreach (($posCashStmt->fetchAll() ?: []) as $row) {
        $movementType = app_normalize_whitespace((string) ($row['movement_type'] ?? 'cash_in'));
        $amount = max(0, app_number($row['total_amount'] ?? 0));
        if ($amount <= 0) {
            continue;
        }
        if ($movementType === 'cash_out') {
            $outTotal += $amount;
            $direction = 'out';
            $method = 'POS Cash Out';
        } else {
            $inTotal += $amount;
            $direction = 'in';
            $method = 'POS Cash In';
        }
        $breakdown[] = [
            'direction' => $direction,
            'payment_method' => $method,
            'amount' => $amount,
        ];
    }

    return [
        'from_date' => $fromDate,
        'to_date' => $toDate,
        'inflow' => $inTotal,
        'outflow' => $outTotal,
        'net_cash' => $inTotal - $outTotal,
        'breakdown' => $breakdown,
    ];
}

function app_companies(bool $onlyActive = true): array
{
    $pdo = app_pdo();
    $sql = 'SELECT * FROM companies';
    if ($onlyActive) {
        $sql .= ' WHERE is_active = 1';
    }
    $sql .= ' ORDER BY name ASC';
    return $pdo->query($sql)->fetchAll() ?: [];
}

function app_company_by_id(int $id): ?array
{
    if ($id <= 0) {
        return null;
    }
    $pdo = app_pdo();
    $stmt = $pdo->prepare('SELECT * FROM companies WHERE id = :id LIMIT 1');
    $stmt->execute(['id' => $id]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_company_logo_path(?array $company = null): string
{
    $logo = is_array($company) ? app_normalize_whitespace((string) ($company['logo_path'] ?? '')) : '';
    if ($logo !== '') {
        return $logo;
    }
    return 'assets/logo.svg';
}

function app_login_attempt(string $username, string $password): bool
{
    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT u.*, c.name AS company_name, c.logo_path AS company_logo
         FROM users u
         LEFT JOIN companies c ON c.id = u.company_id
         WHERE u.username = :username AND u.is_active = 1
         LIMIT 1'
    );
    $stmt->execute(['username' => $username]);
    $user = $stmt->fetch();
    if (!$user) {
        return false;
    }

    if (!password_verify($password, (string) $user['password_hash'])) {
        return false;
    }

    app_login_user($user);
    return true;
}

function app_module_definitions(): array
{
    return [
        'companies' => [
            'title' => 'Companies',
            'icon' => '*',
            'table' => 'companies',
            'admin_only' => true,
            'scoped' => false,
            'description' => 'Manage legal entities and branding.',
            'fields' => [
                ['name' => 'name', 'label' => 'Name', 'type' => 'text', 'required' => true],
                ['name' => 'legal_name', 'label' => 'Legal Name', 'type' => 'text'],
                ['name' => 'vat_number', 'label' => 'VAT Number (15 digits)', 'type' => 'text', 'required' => true],
                ['name' => 'logo_path', 'label' => 'Logo Path', 'type' => 'text'],
                ['name' => 'currency_code', 'label' => 'Currency', 'type' => 'text', 'required' => true],
                ['name' => 'fiscal_year_start', 'label' => 'Fiscal Start (MM-DD)', 'type' => 'text', 'required' => true],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['name', 'legal_name', 'vat_number', 'currency_code', 'fiscal_year_start', 'is_active', 'created_at'],
            'search_fields' => ['name', 'legal_name', 'vat_number', 'currency_code'],
        ],
        'users' => [
            'title' => 'Users',
            'icon' => '*',
            'table' => 'users',
            'admin_only' => true,
            'scoped' => false,
            'description' => 'Control access and role assignments.',
            'fields' => [
                ['name' => 'username', 'label' => 'Username', 'type' => 'text', 'required' => true],
                ['name' => 'password_plain', 'label' => 'Password', 'type' => 'password'],
                ['name' => 'full_name', 'label' => 'Full Name', 'type' => 'text', 'required' => true],
                ['name' => 'role', 'label' => 'Role', 'type' => 'select', 'lookup' => 'role_code', 'required' => true],
                ['name' => 'company_id', 'label' => 'Primary Company', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['username', 'full_name', 'role', 'company_id', 'company_access', 'is_active', 'created_at'],
            'search_fields' => ['username', 'full_name', 'role'],
        ],
        'roles' => [
            'title' => 'Roles',
            'icon' => 'RL',
            'table' => 'roles',
            'admin_only' => true,
            'scoped' => false,
            'description' => 'Create security roles such as accountant, cashier, reviewer, or manager.',
            'fields' => [
                ['name' => 'role_code', 'label' => 'Role Code', 'type' => 'text', 'required' => true],
                ['name' => 'role_name', 'label' => 'Role Name', 'type' => 'text', 'required' => true],
                ['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['role_code', 'role_name', 'is_system', 'is_active', 'created_at'],
            'search_fields' => ['role_code', 'role_name', 'description'],
        ],
        'role_permissions' => [
            'title' => 'Role Permissions',
            'icon' => 'RP',
            'table' => 'role_permissions',
            'admin_only' => true,
            'scoped' => false,
            'description' => 'Control module access, editing, deleting, imports, and posting per role.',
            'fields' => [
                ['name' => 'role_code', 'label' => 'Role', 'type' => 'select', 'lookup' => 'role_code', 'required' => true],
                ['name' => 'module_slug', 'label' => 'Module', 'type' => 'select', 'lookup' => 'module_slug', 'required' => true],
                ['name' => 'can_access', 'label' => 'Can Access', 'type' => 'checkbox'],
                ['name' => 'can_create', 'label' => 'Can Create', 'type' => 'checkbox'],
                ['name' => 'can_edit', 'label' => 'Can Edit', 'type' => 'checkbox'],
                ['name' => 'can_delete', 'label' => 'Can Delete', 'type' => 'checkbox'],
                ['name' => 'can_import', 'label' => 'Can Import', 'type' => 'checkbox'],
                ['name' => 'can_post', 'label' => 'Can Post / Confirm', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['role_code', 'module_slug', 'can_access', 'can_create', 'can_edit', 'can_delete', 'can_import', 'can_post'],
            'search_fields' => ['role_code', 'module_slug', 'notes'],
        ],
        'themes' => [
            'title' => 'Themes',
            'icon' => 'TH',
            'table' => 'themes',
            'admin_only' => true,
            'scoped' => false,
            'description' => 'Choose the active system interface theme while keeping the original Orion UI available.',
            'fields' => [
                ['name' => 'theme_code', 'label' => 'Theme Code', 'type' => 'text', 'required' => true],
                ['name' => 'theme_name', 'label' => 'Theme Name', 'type' => 'text', 'required' => true],
                ['name' => 'ui_skin', 'label' => 'UI Style', 'type' => 'select', 'required' => true, 'options' => [
                    'classic' => 'Original Orion UI',
                    'finance' => 'Develop Finance UI',
                ]],
                ['name' => 'color_mode', 'label' => 'Color Mode', 'type' => 'select', 'required' => true, 'options' => [
                    'light' => 'Light',
                    'dark' => 'Dark',
                ]],
                ['name' => 'is_default', 'label' => 'Use As Current Theme', 'type' => 'checkbox'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['theme_code', 'theme_name', 'ui_skin', 'color_mode', 'is_default', 'is_active'],
            'search_fields' => ['theme_code', 'theme_name', 'ui_skin', 'color_mode', 'notes'],
        ],
        'chart_of_accounts' => [
            'title' => 'Chart of Accounts',
            'icon' => '*',
            'table' => 'chart_of_accounts',
            'description' => 'Create the accounting tree and opening balances.',
            'fields' => [
                ['name' => 'account_code', 'label' => 'Account Code', 'type' => 'text', 'required' => true],
                ['name' => 'account_name', 'label' => 'Account Name', 'type' => 'text', 'required' => true],
                ['name' => 'account_type', 'label' => 'Type', 'type' => 'select', 'required' => true, 'options' => [
                    'asset' => 'Asset',
                    'liability' => 'Liability',
                    'equity' => 'Equity',
                    'revenue' => 'Revenue',
                    'expense' => 'Expense',
                ]],
                ['name' => 'parent_code', 'label' => 'Parent Code', 'type' => 'select', 'lookup' => 'chart_parent_code'],
                ['name' => 'opening_balance', 'label' => 'Opening Balance', 'type' => 'number'],
                ['name' => 'financial_statement_group_code', 'label' => 'Financial Statement Group', 'type' => 'select', 'lookup' => 'financial_statement_group_code'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['account_code', 'account_name', 'account_type', 'financial_statement_group_code', 'opening_balance', 'is_active'],
            'search_fields' => ['account_code', 'account_name', 'account_type', 'financial_statement_group_code'],
        ],
        'financial_statement_groups' => [
            'title' => 'Financial Statement Groups',
            'icon' => 'FS',
            'table' => 'financial_statement_groups',
            'description' => 'Code statement line groups used by the Income Statement and Statement of Financial Position.',
            'fields' => [
                ['name' => 'group_code', 'label' => 'Financial Statement Group Code', 'type' => 'text', 'required' => true],
                ['name' => 'group_name', 'label' => 'Financial Statement Group Name', 'type' => 'text', 'required' => true],
                ['name' => 'statement_type', 'label' => 'Statement Type', 'type' => 'select', 'required' => true, 'options' => [
                    'balance_sheet' => 'Statement of Financial Position',
                    'income_statement' => 'Income Statement',
                ]],
                ['name' => 'main_section', 'label' => 'Main Section', 'type' => 'select', 'required' => true, 'options' => [
                    'bs_non_current_assets' => 'Non-current Assets',
                    'bs_current_assets' => 'Current Assets',
                    'bs_equity' => 'Equity',
                    'bs_non_current_liabilities' => 'Non-current Liabilities',
                    'bs_current_liabilities' => 'Current Liabilities',
                    'is_revenue' => 'Revenue',
                    'is_revenue_deductions' => 'Revenue Deductions',
                    'is_cost_of_revenue' => 'Cost of Revenue',
                    'is_operating_expenses' => 'Operating Expenses',
                    'is_other_income' => 'Other Income',
                    'is_finance_costs' => 'Finance Costs',
                    'is_income_tax' => 'Income Tax',
                ]],
                ['name' => 'sort_order', 'label' => 'Sort Order', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['group_code', 'group_name', 'statement_type', 'main_section', 'sort_order', 'is_active'],
            'search_fields' => ['group_code', 'group_name', 'statement_type', 'main_section'],
        ],
        'ecl_countries' => [
            'title' => 'ECL Countries',
            'icon' => 'EC',
            'table' => 'ecl_countries',
            'description' => 'Country ratings, macro scenario factors, and scenario weights for expected credit loss reports.',
            'fields' => [
                ['name' => 'country_code', 'label' => 'Country Code', 'type' => 'text', 'required' => true],
                ['name' => 'country_name', 'label' => 'Country Name', 'type' => 'text', 'required' => true],
                ['name' => 'sp_rating', 'label' => 'S&P Rating', 'type' => 'text'],
                ['name' => 'moodys_rating', 'label' => "Moody's Rating", 'type' => 'text'],
                ['name' => 'fitch_rating', 'label' => 'Fitch Rating', 'type' => 'text'],
                ['name' => 'base_scenario_factor', 'label' => 'Base Scenario Factor', 'type' => 'number'],
                ['name' => 'best_scenario_factor', 'label' => 'Best Scenario Factor', 'type' => 'number'],
                ['name' => 'worst_scenario_factor', 'label' => 'Worst Scenario Factor', 'type' => 'number'],
                ['name' => 'base_weight_percent', 'label' => 'Base Weight %', 'type' => 'number'],
                ['name' => 'best_weight_percent', 'label' => 'Best Weight %', 'type' => 'number'],
                ['name' => 'worst_weight_percent', 'label' => 'Worst Weight %', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['country_code', 'country_name', 'sp_rating', 'moodys_rating', 'fitch_rating', 'base_weight_percent', 'best_weight_percent', 'worst_weight_percent', 'is_active'],
            'search_fields' => ['country_code', 'country_name', 'sp_rating', 'moodys_rating', 'fitch_rating'],
        ],
        'ecl_default_rates' => [
            'title' => 'ECL Default Rates',
            'icon' => 'DR',
            'table' => 'ecl_default_rates',
            'description' => 'Observed one-year probability of default by rating agency and rating code.',
            'fields' => [
                ['name' => 'agency', 'label' => 'Agency', 'type' => 'select', 'required' => true, 'options' => [
                    'sp' => 'S&P',
                    'moodys' => "Moody's",
                    'fitch' => 'Fitch',
                ]],
                ['name' => 'rating_code', 'label' => 'Rating Code', 'type' => 'text', 'required' => true],
                ['name' => 'one_year_pd_percent', 'label' => 'One-Year PD %', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['agency', 'rating_code', 'one_year_pd_percent', 'is_active'],
            'search_fields' => ['agency', 'rating_code'],
        ],
        'ecl_receivable_buckets' => [
            'title' => 'ECL Receivable Buckets',
            'icon' => 'RB',
            'table' => 'ecl_receivable_buckets',
            'description' => 'Aging buckets and forward-looking loss rates used for sales receivables.',
            'fields' => [
                ['name' => 'bucket_code', 'label' => 'Bucket Code', 'type' => 'text', 'required' => true],
                ['name' => 'bucket_name', 'label' => 'Bucket Name', 'type' => 'text', 'required' => true],
                ['name' => 'min_days', 'label' => 'Min Days', 'type' => 'number'],
                ['name' => 'max_days', 'label' => 'Max Days (0 = open)', 'type' => 'number'],
                ['name' => 'loss_rate_percent', 'label' => 'Loss Rate %', 'type' => 'number'],
                ['name' => 'sort_order', 'label' => 'Sort Order', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['bucket_code', 'bucket_name', 'min_days', 'max_days', 'loss_rate_percent', 'sort_order', 'is_active'],
            'search_fields' => ['bucket_code', 'bucket_name'],
        ],
        'ecl_report_categories' => [
            'title' => 'ECL Report Categories',
            'icon' => 'ECL',
            'table' => 'ecl_report_categories',
            'description' => 'Map report lines to ERP sources, account balances, and ECL loss models.',
            'fields' => [
                ['name' => 'category_code', 'label' => 'Category Code', 'type' => 'text', 'required' => true],
                ['name' => 'category_name', 'label' => 'Category Name', 'type' => 'text', 'required' => true],
                ['name' => 'source_type', 'label' => 'Source Type', 'type' => 'select', 'required' => true, 'options' => [
                    'bank_accounts' => 'Bank Accounts',
                    'sales_receivables' => 'Sales Receivables',
                    'account_balances' => 'Account Balances',
                    'manual_amount' => 'Manual Amount',
                ]],
                ['name' => 'loss_model', 'label' => 'Loss Model', 'type' => 'select', 'required' => true, 'options' => [
                    'country_12m' => '12-Month Country Forward-Looking',
                    'receivable_aging' => 'Receivable Aging Buckets',
                    'manual_rate' => 'Manual Rate Override',
                ]],
                ['name' => 'country_code', 'label' => 'Country Override', 'type' => 'select', 'lookup' => 'ecl_country_code'],
                ['name' => 'financial_statement_group_code', 'label' => 'Financial Statement Group', 'type' => 'select', 'lookup' => 'financial_statement_group_code'],
                ['name' => 'account_codes', 'label' => 'Account Codes (comma separated)', 'type' => 'textarea'],
                ['name' => 'manual_amount', 'label' => 'Manual Amount', 'type' => 'number'],
                ['name' => 'lgd_percent', 'label' => 'LGD %', 'type' => 'number'],
                ['name' => 'rate_override_percent', 'label' => 'Rate Override %', 'type' => 'number'],
                ['name' => 'sort_order', 'label' => 'Sort Order', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['category_code', 'category_name', 'source_type', 'loss_model', 'country_code', 'lgd_percent', 'rate_override_percent', 'sort_order', 'is_active'],
            'search_fields' => ['category_code', 'category_name', 'source_type', 'loss_model', 'account_codes'],
        ],
        'posting_account_settings' => [
            'title' => 'Posting Accounts',
            'icon' => '*',
            'table' => 'posting_account_settings',
            'description' => 'Map default AR, AP, accrued, prepaid, depreciation, and IFRS 16 lease accounts per company.',
            'fields' => [
                ['name' => 'sales_receivable_account_code', 'label' => 'Sales Receivable Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'purchase_payable_account_code', 'label' => 'Purchase Payable Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'accrued_expense_account_code', 'label' => 'Accrued Expense Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'prepaid_expense_account_code', 'label' => 'Prepaid Expense Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'ifrs16_rou_asset_account_code', 'label' => 'IFRS16 ROU Asset Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'ifrs16_lease_liability_account_code', 'label' => 'IFRS16 Lease Liability Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'ifrs16_interest_expense_account_code', 'label' => 'IFRS16 Interest Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'ifrs16_depreciation_expense_account_code', 'label' => 'IFRS16 Depreciation Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'ifrs16_accumulated_depreciation_account_code', 'label' => 'IFRS16 Accumulated Depreciation Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'ifrs16_restoration_provision_account_code', 'label' => 'IFRS16 Restoration Provision Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'ifrs16_lease_incentive_account_code', 'label' => 'IFRS16 Lease Incentive Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'pos_sales_revenue_account_code', 'label' => 'POS Sales Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code', 'required' => true],
                ['name' => 'pos_cash_over_short_gain_account_code', 'label' => 'POS Cash Overage Gain Account', 'type' => 'select', 'lookup' => 'revenue_account_code', 'required' => true],
                ['name' => 'pos_cash_over_short_loss_account_code', 'label' => 'POS Cash Shortage Loss Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['sales_receivable_account_code', 'purchase_payable_account_code', 'accrued_expense_account_code', 'prepaid_expense_account_code', 'pos_sales_revenue_account_code', 'pos_cash_over_short_gain_account_code', 'pos_cash_over_short_loss_account_code', 'ifrs16_rou_asset_account_code'],
            'search_fields' => ['sales_receivable_account_code', 'purchase_payable_account_code', 'accrued_expense_account_code', 'prepaid_expense_account_code', 'pos_sales_revenue_account_code', 'pos_cash_over_short_gain_account_code', 'pos_cash_over_short_loss_account_code', 'ifrs16_rou_asset_account_code', 'ifrs16_lease_liability_account_code', 'ifrs16_interest_expense_account_code'],
        ],
        'contacts' => [
            'title' => 'Contacts',
            'icon' => '*',
            'table' => 'contacts',
            'description' => 'Maintain customers, vendors, and balances.',
            'fields' => [
                ['name' => 'contact_type', 'label' => 'Type', 'type' => 'select', 'required' => true, 'options' => [
                    'customer' => 'Customer',
                    'vendor' => 'Vendor',
                    'both' => 'Both',
                ]],
                ['name' => 'contact_code', 'label' => 'Code', 'type' => 'text', 'required' => true],
                ['name' => 'contact_name', 'label' => 'Name', 'type' => 'text', 'required' => true],
                ['name' => 'tax_number', 'label' => 'Tax Number', 'type' => 'text'],
                ['name' => 'withholding_tax_name', 'label' => 'Commercial Profit Tax Type', 'type' => 'select', 'lookup' => 'withholding_tax_name'],
                ['name' => 'withholding_tax_rate_percent', 'label' => 'Commercial Profit Tax %', 'type' => 'number'],
                ['name' => 'phone', 'label' => 'Phone', 'type' => 'text'],
                ['name' => 'email', 'label' => 'Email', 'type' => 'text'],
                ['name' => 'address', 'label' => 'Address', 'type' => 'textarea'],
                ['name' => 'opening_balance', 'label' => 'Opening Balance', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['contact_type', 'contact_code', 'contact_name', 'withholding_tax_name', 'withholding_tax_rate_percent', 'phone', 'opening_balance', 'is_active'],
            'search_fields' => ['contact_code', 'contact_name', 'phone', 'email', 'withholding_tax_name'],
        ],
        'products' => [
            'title' => 'Products',
            'icon' => '*',
            'table' => 'products',
            'description' => 'Products catalog and stock balances.',
            'fields' => [
                ['name' => 'sku', 'label' => 'SKU', 'type' => 'text', 'required' => true],
                ['name' => 'product_name', 'label' => 'Name', 'type' => 'text', 'required' => true],
                ['name' => 'category', 'label' => 'Category', 'type' => 'text'],
                ['name' => 'unit_name', 'label' => 'Unit', 'type' => 'text'],
                ['name' => 'sale_price', 'label' => 'Sale Price', 'type' => 'number'],
                ['name' => 'purchase_price', 'label' => 'Purchase Price', 'type' => 'number'],
                ['name' => 'qty_on_hand', 'label' => 'Qty On Hand', 'type' => 'number'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax %', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['sku', 'product_name', 'category', 'qty_on_hand', 'sale_price', 'is_active'],
            'search_fields' => ['sku', 'product_name', 'category'],
        ],
        'tax_rates' => [
            'title' => 'Tax Rates',
            'icon' => '*',
            'table' => 'tax_rates',
            'description' => 'Tax definitions for sales and purchases.',
            'fields' => [
                ['name' => 'tax_name', 'label' => 'Tax Name', 'type' => 'text', 'required' => true],
                ['name' => 'rate_percent', 'label' => 'Rate %', 'type' => 'number', 'required' => true],
                ['name' => 'applies_to', 'label' => 'Applies To', 'type' => 'select', 'required' => true, 'options' => [
                    'sales' => 'Sales',
                    'purchase' => 'Purchase',
                    'withholding' => 'Withholding / Commercial Profit Tax',
                    'both' => 'Both',
                ]],
                ['name' => 'tax_account_code', 'label' => 'Tax Posting Account', 'type' => 'select', 'lookup' => 'chart_account_code'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['tax_name', 'rate_percent', 'applies_to', 'tax_account_code', 'is_active'],
            'search_fields' => ['tax_name', 'applies_to', 'tax_account_code'],
        ],
        'bank_accounts' => [
            'title' => 'Bank Accounts',
            'icon' => '*',
            'table' => 'bank_accounts',
            'description' => 'Manage treasury and bank balances.',
            'fields' => [
                ['name' => 'account_name', 'label' => 'Account Name', 'type' => 'text', 'required' => true],
                ['name' => 'bank_name', 'label' => 'Bank Name', 'type' => 'text'],
                ['name' => 'gl_account_code', 'label' => 'GL Account', 'type' => 'select', 'lookup' => 'asset_account_code'],
                ['name' => 'iban', 'label' => 'IBAN', 'type' => 'text'],
                ['name' => 'account_number', 'label' => 'Account Number', 'type' => 'text'],
                ['name' => 'opening_balance', 'label' => 'Opening Balance', 'type' => 'number'],
                ['name' => 'current_balance', 'label' => 'Current Balance', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['account_name', 'bank_name', 'account_number', 'current_balance', 'is_active'],
            'search_fields' => ['account_name', 'bank_name', 'account_number'],
        ],
        'cash_accounts' => [
            'title' => 'Cash Accounts',
            'icon' => '*',
            'table' => 'cash_accounts',
            'description' => 'Manage cash boxes, petty cash custodians, and cash balances.',
            'fields' => [
                ['name' => 'cash_name', 'label' => 'Cash Name', 'type' => 'text', 'required' => true],
                ['name' => 'gl_account_code', 'label' => 'GL Account', 'type' => 'select', 'lookup' => 'asset_account_code'],
                ['name' => 'custodian_name', 'label' => 'Custodian', 'type' => 'text'],
                ['name' => 'location_name', 'label' => 'Location', 'type' => 'text'],
                ['name' => 'opening_balance', 'label' => 'Opening Balance', 'type' => 'number'],
                ['name' => 'current_balance', 'label' => 'Current Balance', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['cash_name', 'gl_account_code', 'custodian_name', 'location_name', 'current_balance', 'is_active'],
            'search_fields' => ['cash_name', 'custodian_name', 'location_name'],
        ],
        'pos_registers' => [
            'title' => 'POS Registers',
            'icon' => 'POS',
            'table' => 'pos_registers',
            'description' => 'Configure point-of-sale registers, cash boxes, receipt behavior, and posting defaults.',
            'fields' => [
                ['name' => 'register_code', 'label' => 'Register Code', 'type' => 'text', 'required' => true],
                ['name' => 'register_name', 'label' => 'Register Name', 'type' => 'text', 'required' => true],
                ['name' => 'cash_account_name', 'label' => 'Cash Drawer', 'type' => 'select', 'lookup' => 'cash_account_name', 'required' => true],
                ['name' => 'default_bank_account_name', 'label' => 'Default Bank/Card Account', 'type' => 'select', 'lookup' => 'bank_account_name'],
                ['name' => 'default_customer_code', 'label' => 'Default Customer', 'type' => 'select', 'lookup' => 'customer_contact_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'sales_revenue_account_code', 'label' => 'Sales Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'price_includes_tax', 'label' => 'Prices Include Tax', 'type' => 'checkbox'],
                ['name' => 'allow_negative_stock', 'label' => 'Allow Negative Stock', 'type' => 'checkbox'],
                ['name' => 'receipt_footer', 'label' => 'Receipt Footer', 'type' => 'textarea'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['register_code', 'register_name', 'cash_account_name', 'default_bank_account_name', 'cost_center_code', 'price_includes_tax', 'allow_negative_stock', 'is_active'],
            'search_fields' => ['register_code', 'register_name', 'cash_account_name', 'default_bank_account_name'],
        ],
        'pos_sessions' => [
            'title' => 'POS Sessions',
            'icon' => 'PSS',
            'table' => 'pos_sessions',
            'description' => 'Cashier opening and closing sessions with expected cash and cash difference tracking.',
            'fields' => [
                ['name' => 'session_number', 'label' => 'Session Number', 'type' => 'text', 'required' => true],
                ['name' => 'register_id', 'label' => 'Register', 'type' => 'select', 'lookup' => 'pos_register_id', 'required' => true],
                ['name' => 'opened_at', 'label' => 'Opened At', 'type' => 'text', 'required' => true],
                ['name' => 'closed_at', 'label' => 'Closed At', 'type' => 'text'],
                ['name' => 'opening_cash_counted', 'label' => 'Opening Cash', 'type' => 'number'],
                ['name' => 'expected_cash_amount', 'label' => 'Expected Cash', 'type' => 'number'],
                ['name' => 'closing_cash_counted', 'label' => 'Closing Cash Counted', 'type' => 'number'],
                ['name' => 'cash_difference', 'label' => 'Cash Difference', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'open' => 'Open',
                    'closed' => 'Closed',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['session_number', 'register_id', 'opened_at', 'closed_at', 'opening_cash_counted', 'expected_cash_amount', 'closing_cash_counted', 'cash_difference', 'status'],
            'search_fields' => ['session_number', 'status', 'notes'],
        ],
        'pos_orders' => [
            'title' => 'POS Orders',
            'icon' => 'PO',
            'table' => 'pos_orders',
            'description' => 'Posted point-of-sale orders, receipts, refunds, split payments, and journal links.',
            'fields' => [
                ['name' => 'order_number', 'label' => 'Order Number', 'type' => 'text', 'required' => true],
                ['name' => 'receipt_number', 'label' => 'Receipt Number', 'type' => 'text'],
                ['name' => 'session_id', 'label' => 'Session ID', 'type' => 'number', 'required' => true],
                ['name' => 'order_date', 'label' => 'Order Date', 'type' => 'text', 'required' => true],
                ['name' => 'order_type', 'label' => 'Order Type', 'type' => 'select', 'options' => [
                    'sale' => 'Sale',
                    'refund' => 'Refund',
                ]],
                ['name' => 'original_order_number', 'label' => 'Original Order', 'type' => 'text'],
                ['name' => 'customer_code', 'label' => 'Customer', 'type' => 'select', 'lookup' => 'customer_contact_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number'],
                ['name' => 'paid_total', 'label' => 'Paid Total', 'type' => 'number'],
                ['name' => 'change_amount', 'label' => 'Change', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'paid' => 'Paid',
                    'refunded' => 'Refunded',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'invoice_requested', 'label' => 'Invoice Requested', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['order_number', 'receipt_number', 'order_date', 'order_type', 'customer_code', 'subtotal', 'tax_total', 'grand_total', 'paid_total', 'status'],
            'search_fields' => ['order_number', 'receipt_number', 'order_type', 'original_order_number', 'customer_code', 'status', 'notes'],
        ],
        'pos_cash_movements' => [
            'title' => 'POS Cash In/Out',
            'icon' => 'CIO',
            'table' => 'pos_cash_movements',
            'description' => 'Register cash adjustments posted directly to the general ledger.',
            'fields' => [
                ['name' => 'movement_number', 'label' => 'Movement Number', 'type' => 'text', 'required' => true],
                ['name' => 'session_id', 'label' => 'Session ID', 'type' => 'number', 'required' => true],
                ['name' => 'movement_date', 'label' => 'Movement Date', 'type' => 'text', 'required' => true],
                ['name' => 'movement_type', 'label' => 'Type', 'type' => 'select', 'required' => true, 'options' => [
                    'cash_in' => 'Cash In',
                    'cash_out' => 'Cash Out',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Cash Drawer', 'type' => 'select', 'lookup' => 'cash_account_name', 'required' => true],
                ['name' => 'offset_account_code', 'label' => 'Offset Account', 'type' => 'select', 'lookup' => 'chart_account_code', 'required' => true],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'reason', 'label' => 'Reason', 'type' => 'text'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
            ],
            'list_columns' => ['movement_number', 'session_id', 'movement_date', 'movement_type', 'payment_account_name', 'offset_account_code', 'amount', 'status'],
            'search_fields' => ['movement_number', 'movement_type', 'payment_account_name', 'offset_account_code', 'reason', 'status'],
        ],
        'cost_centers' => [
            'title' => 'Cost Centers',
            'icon' => '*',
            'table' => 'cost_centers',
            'description' => 'Track costs by department or business line.',
            'fields' => [
                ['name' => 'center_code', 'label' => 'Code', 'type' => 'text', 'required' => true],
                ['name' => 'center_name', 'label' => 'Name', 'type' => 'text', 'required' => true],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['center_code', 'center_name', 'is_active'],
            'search_fields' => ['center_code', 'center_name'],
        ],
        'projects' => [
            'title' => 'Projects',
            'icon' => '*',
            'table' => 'projects',
            'description' => 'Project accounting and status tracking.',
            'fields' => [
                ['name' => 'project_code', 'label' => 'Code', 'type' => 'text', 'required' => true],
                ['name' => 'project_name', 'label' => 'Name', 'type' => 'text', 'required' => true],
                ['name' => 'client_name', 'label' => 'Client', 'type' => 'text'],
                ['name' => 'start_date', 'label' => 'Start Date', 'type' => 'date'],
                ['name' => 'end_date', 'label' => 'End Date', 'type' => 'date'],
                ['name' => 'budget_amount', 'label' => 'Budget', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'planned' => 'Planned',
                    'active' => 'Active',
                    'on_hold' => 'On Hold',
                    'closed' => 'Closed',
                ]],
            ],
            'list_columns' => ['project_code', 'project_name', 'client_name', 'budget_amount', 'status'],
            'search_fields' => ['project_code', 'project_name', 'client_name', 'status'],
        ],
        'budgets' => [
            'title' => 'Budgets',
            'icon' => '*',
            'table' => 'budgets',
            'description' => 'Annual and departmental budgeting records.',
            'fields' => [
                ['name' => 'fiscal_year', 'label' => 'Fiscal Year', 'type' => 'text', 'required' => true],
                ['name' => 'budget_name', 'label' => 'Budget Name', 'type' => 'text', 'required' => true],
                ['name' => 'account_code', 'label' => 'Account Code', 'type' => 'select', 'lookup' => 'chart_account_code'],
                ['name' => 'budget_amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['fiscal_year', 'budget_name', 'account_code', 'budget_amount'],
            'search_fields' => ['fiscal_year', 'budget_name', 'account_code'],
        ],
        'journal_entries' => [
            'title' => 'Journal Entries',
            'icon' => '*',
            'table' => 'journal_entries',
            'description' => 'Manual accounting entries and posting status.',
            'fields' => [
                ['name' => 'entry_number', 'label' => 'Entry Number', 'type' => 'text', 'required' => true],
                ['name' => 'entry_date', 'label' => 'Entry Date', 'type' => 'date', 'required' => true],
                ['name' => 'reference', 'label' => 'Reference', 'type' => 'text'],
                ['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
                ['name' => 'total_debit', 'label' => 'Total Debit', 'type' => 'number', 'required' => true],
                ['name' => 'total_credit', 'label' => 'Total Credit', 'type' => 'number', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'reversed' => 'Reversed',
                ]],
            ],
            'list_columns' => ['entry_number', 'entry_date', 'reference', 'source_type', 'source_id', 'line_items_count', 'line_items_total_debit', 'line_items_total_credit', 'total_debit', 'total_credit', 'status'],
            'search_fields' => ['entry_number', 'reference', 'status'],
        ],
        'sales_invoices' => [
            'title' => 'Sales Invoices',
            'icon' => '*',
            'table' => 'sales_invoices',
            'description' => 'Billing, collections, and receivable tracking.',
            'fields' => [
                ['name' => 'invoice_number', 'label' => 'Invoice Number', 'type' => 'text', 'required' => true],
                ['name' => 'invoice_date', 'label' => 'Invoice Date', 'type' => 'date', 'required' => true],
                ['name' => 'customer_code', 'label' => 'Customer', 'type' => 'select', 'lookup' => 'customer_contact_code', 'required' => true],
                ['name' => 'revenue_account_code', 'label' => 'Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Invoice', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'sales_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Collected In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Collection Method', 'type' => 'select', 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Collected In', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'issued' => 'Issued',
                    'partially_paid' => 'Partially Paid',
                    'paid' => 'Paid',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['invoice_number', 'invoice_date', 'customer_code', 'cost_center_code', 'grand_total', 'paid_amount', 'status'],
            'search_fields' => ['invoice_number', 'customer_code', 'status'],
        ],
        'purchase_bills' => [
            'title' => 'Purchase Bills',
            'icon' => '*',
            'table' => 'purchase_bills',
            'description' => 'Vendor bills and payable tracking.',
            'fields' => [
                ['name' => 'bill_number', 'label' => 'Bill Number', 'type' => 'text', 'required' => true],
                ['name' => 'bill_date', 'label' => 'Bill Date', 'type' => 'date', 'required' => true],
                ['name' => 'vendor_code', 'label' => 'Vendor', 'type' => 'select', 'lookup' => 'vendor_contact_code', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Bill', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'purchase_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'withholding_tax_name', 'label' => 'Commercial Profit Tax Type', 'type' => 'select', 'lookup' => 'withholding_tax_name'],
                ['name' => 'withholding_tax_rate_percent', 'label' => 'Commercial Profit Tax %', 'type' => 'number'],
                ['name' => 'withholding_tax_total', 'label' => 'Commercial Profit Tax Amount', 'type' => 'number'],
                ['name' => 'net_payable', 'label' => 'Net Payable', 'type' => 'number'],
                ['name' => 'inline_paid_amount', 'label' => 'Paid In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'approved' => 'Approved',
                    'partially_paid' => 'Partially Paid',
                    'paid' => 'Paid',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['bill_number', 'bill_date', 'vendor_code', 'cost_center_code', 'grand_total', 'withholding_tax_total', 'net_payable', 'paid_amount', 'budget_remaining', 'status'],
            'search_fields' => ['bill_number', 'vendor_code', 'withholding_tax_name', 'status'],
        ],
        'expenses' => [
            'title' => 'Expenses',
            'icon' => '*',
            'table' => 'expenses',
            'description' => 'Direct expenses, petty cash payments, and reimbursable costs.',
            'fields' => [
                ['name' => 'expense_number', 'label' => 'Expense Number', 'type' => 'text', 'required' => true],
                ['name' => 'expense_date', 'label' => 'Expense Date', 'type' => 'date', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Expense', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'purchase_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Paid In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'offset_account_code', 'label' => 'Offset Account (For unpaid / partial only)', 'type' => 'select', 'lookup' => 'liability_account_code'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'approved' => 'Approved',
                    'partially_paid' => 'Partially Paid',
                    'paid' => 'Paid',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['expense_number', 'expense_date', 'cost_center_code', 'grand_total', 'paid_amount', 'budget_remaining', 'status'],
            'search_fields' => ['expense_number', 'status'],
        ],
        'sales_returns' => [
            'title' => 'Sales Returns',
            'icon' => 'SR',
            'table' => 'sales_returns',
            'description' => 'Returns against sales invoices, with full or partial return lines.',
            'fields' => [
                ['name' => 'return_number', 'label' => 'Return Number', 'type' => 'text', 'required' => true],
                ['name' => 'return_date', 'label' => 'Return Date', 'type' => 'date', 'required' => true],
                ['name' => 'return_type', 'label' => 'Return Type', 'type' => 'select', 'options' => ['full' => 'Full Return', 'partial' => 'Partial Return']],
                ['name' => 'original_invoice_id', 'label' => 'Original Invoice', 'type' => 'select', 'lookup' => 'sales_invoice_id'],
                ['name' => 'customer_code', 'label' => 'Customer', 'type' => 'select', 'lookup' => 'customer_contact_code', 'required' => true],
                ['name' => 'revenue_account_code', 'label' => 'Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Invoice', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'sales_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Refund In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Refund Method', 'type' => 'select', 'options' => ['cash' => 'Cash', 'bank' => 'Bank', 'card' => 'Card', 'transfer' => 'Transfer']],
                ['name' => 'payment_account_name', 'label' => 'Refund Account', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => ['draft' => 'Draft', 'issued' => 'Posted', 'partially_paid' => 'Partially Paid', 'paid' => 'Paid', 'cancelled' => 'Cancelled']],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['return_number', 'return_date', 'return_type', 'customer_code', 'grand_total', 'paid_amount', 'status'],
            'search_fields' => ['return_number', 'customer_code', 'return_type', 'status'],
        ],
        'purchase_returns' => [
            'title' => 'Purchase Returns',
            'icon' => 'PR',
            'table' => 'purchase_returns',
            'description' => 'Returns against vendor bills, with full or partial return lines.',
            'fields' => [
                ['name' => 'return_number', 'label' => 'Return Number', 'type' => 'text', 'required' => true],
                ['name' => 'return_date', 'label' => 'Return Date', 'type' => 'date', 'required' => true],
                ['name' => 'return_type', 'label' => 'Return Type', 'type' => 'select', 'options' => ['full' => 'Full Return', 'partial' => 'Partial Return']],
                ['name' => 'original_bill_id', 'label' => 'Original Bill', 'type' => 'select', 'lookup' => 'purchase_bill_id'],
                ['name' => 'vendor_code', 'label' => 'Vendor', 'type' => 'select', 'lookup' => 'vendor_contact_code', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Bill', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'purchase_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Refund In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Refund Method', 'type' => 'select', 'options' => ['cash' => 'Cash', 'bank' => 'Bank', 'card' => 'Card', 'transfer' => 'Transfer']],
                ['name' => 'payment_account_name', 'label' => 'Refund Account', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => ['draft' => 'Draft', 'approved' => 'Posted', 'partially_paid' => 'Partially Paid', 'paid' => 'Paid', 'cancelled' => 'Cancelled']],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['return_number', 'return_date', 'return_type', 'vendor_code', 'grand_total', 'paid_amount', 'status'],
            'search_fields' => ['return_number', 'vendor_code', 'return_type', 'status'],
        ],
        'expense_returns' => [
            'title' => 'Expense Returns',
            'icon' => 'ER',
            'table' => 'expense_returns',
            'description' => 'Returns against direct expenses, with full or partial return lines.',
            'fields' => [
                ['name' => 'return_number', 'label' => 'Return Number', 'type' => 'text', 'required' => true],
                ['name' => 'return_date', 'label' => 'Return Date', 'type' => 'date', 'required' => true],
                ['name' => 'return_type', 'label' => 'Return Type', 'type' => 'select', 'options' => ['full' => 'Full Return', 'partial' => 'Partial Return']],
                ['name' => 'original_expense_id', 'label' => 'Original Expense', 'type' => 'select', 'lookup' => 'expense_id'],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Expense', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'purchase_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Refund In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Refund Method', 'type' => 'select', 'options' => ['cash' => 'Cash', 'bank' => 'Bank', 'card' => 'Card', 'transfer' => 'Transfer']],
                ['name' => 'payment_account_name', 'label' => 'Refund Account', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'offset_account_code', 'label' => 'Offset Account (For unpaid / partial only)', 'type' => 'select', 'lookup' => 'liability_account_code'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => ['draft' => 'Draft', 'approved' => 'Posted', 'partially_paid' => 'Partially Paid', 'paid' => 'Paid', 'cancelled' => 'Cancelled']],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['return_number', 'return_date', 'return_type', 'grand_total', 'paid_amount', 'status'],
            'search_fields' => ['return_number', 'return_type', 'status'],
        ],
        'prepaid_expenses' => [
            'title' => 'Prepaid Expenses',
            'icon' => '*',
            'table' => 'prepaid_expenses',
            'description' => 'Capitalize prepaid costs and generate monthly amortization as draft journal entries.',
            'fields' => [
                ['name' => 'prepaid_number', 'label' => 'Prepaid Number', 'type' => 'text', 'required' => true],
                ['name' => 'prepaid_date', 'label' => 'Prepaid Date', 'type' => 'date', 'required' => true],
                ['name' => 'description', 'label' => 'Description', 'type' => 'text', 'required' => true],
                ['name' => 'contact_code', 'label' => 'Contact', 'type' => 'select', 'lookup' => 'all_contact_code'],
                ['name' => 'prepaid_account_code', 'label' => 'Prepaid Account', 'type' => 'select', 'lookup' => 'asset_account_code'],
                ['name' => 'expense_account_code', 'label' => 'Amortization Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'total_amount', 'label' => 'Prepaid Base Amount', 'type' => 'number', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Prepaid Expense', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'purchase_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number'],
                ['name' => 'inline_paid_amount', 'label' => 'Paid In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'start_date', 'label' => 'Amortization Start Date', 'type' => 'date', 'required' => true],
                ['name' => 'duration_months', 'label' => 'Duration (Months)', 'type' => 'number', 'required' => true],
                ['name' => 'monthly_amount', 'label' => 'Monthly Amount', 'type' => 'number'],
                ['name' => 'amortized_amount', 'label' => 'Generated Amortization', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'active' => 'Active',
                    'closed' => 'Closed',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['prepaid_number', 'prepaid_date', 'description', 'prepaid_account_code', 'expense_account_code', 'total_amount', 'tax_total', 'grand_total', 'amortized_amount', 'status'],
            'search_fields' => ['prepaid_number', 'description', 'contact_code', 'status'],
        ],
        'prepaid_amortization_runs' => [
            'title' => 'Prepaid Amortization',
            'icon' => '*',
            'table' => 'prepaid_amortization_runs',
            'description' => 'Generate draft amortization entries: Dr expense / Cr prepaid asset.',
            'fields' => [
                ['name' => 'run_number', 'label' => 'Run Number', 'type' => 'text', 'required' => true],
                ['name' => 'run_date', 'label' => 'Run Date', 'type' => 'date', 'required' => true],
                ['name' => 'period_month', 'label' => 'Period Month', 'type' => 'number', 'required' => true],
                ['name' => 'period_year', 'label' => 'Period Year', 'type' => 'number', 'required' => true],
                ['name' => 'prepaid_expense_id', 'label' => 'Prepaid Expense', 'type' => 'select', 'lookup' => 'prepaid_expense_id', 'required' => true],
                ['name' => 'prepaid_number', 'label' => 'Prepaid Number', 'type' => 'text'],
                ['name' => 'month_no', 'label' => 'Month No', 'type' => 'number'],
                ['name' => 'prepaid_account_code', 'label' => 'Prepaid Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['run_number', 'run_date', 'prepaid_number', 'month_no', 'expense_account_code', 'cost_center_code', 'amount', 'status'],
            'search_fields' => ['run_number', 'prepaid_number', 'expense_account_code', 'status'],
        ],
        'payments' => [
            'title' => 'Payments',
            'icon' => '*',
            'table' => 'payments',
            'description' => 'Cash and bank transactions.',
            'fields' => [
                ['name' => 'payment_number', 'label' => 'Payment Number', 'type' => 'text', 'required' => true],
                ['name' => 'payment_date', 'label' => 'Payment Date', 'type' => 'date', 'required' => true],
                ['name' => 'direction', 'label' => 'Direction', 'type' => 'select', 'required' => true, 'options' => [
                    'in' => 'Incoming',
                    'out' => 'Outgoing',
                ]],
                ['name' => 'payment_method', 'label' => 'Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'reference', 'label' => 'Reference', 'type' => 'text'],
                ['name' => 'sales_invoice_id', 'label' => 'Sales Invoice', 'type' => 'select', 'lookup' => 'sales_invoice_id'],
                ['name' => 'purchase_bill_id', 'label' => 'Vendor Bill', 'type' => 'select', 'lookup' => 'purchase_bill_id'],
                ['name' => 'expense_id', 'label' => 'Expense', 'type' => 'select', 'lookup' => 'expense_id'],
                ['name' => 'ifrs16_lease_id', 'label' => 'IFRS16 Lease', 'type' => 'select', 'lookup' => 'ifrs16_lease_id'],
                ['name' => 'ifrs16_schedule_id', 'label' => 'IFRS16 Payment Line', 'type' => 'select', 'lookup' => 'ifrs16_schedule_id'],
                ['name' => 'contact_code', 'label' => 'Contact', 'type' => 'select', 'lookup' => 'all_contact_code'],
                ['name' => 'bank_account_name', 'label' => 'Payment Account', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'offset_account_code', 'label' => 'Offset Account (Optional)', 'type' => 'select', 'lookup' => 'chart_account_code'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['payment_number', 'payment_date', 'direction', 'payment_method', 'amount', 'sales_invoice_id', 'purchase_bill_id', 'expense_id', 'ifrs16_lease_id', 'ifrs16_schedule_id', 'contact_code'],
            'search_fields' => ['payment_number', 'reference', 'sales_invoice_id', 'purchase_bill_id', 'expense_id', 'ifrs16_lease_id', 'ifrs16_schedule_id', 'contact_code', 'payment_method'],
        ],
        'assets' => [
            'title' => 'Fixed Assets',
            'icon' => '*',
            'table' => 'assets',
            'description' => 'Assets register and depreciation fields.',
            'fields' => [
                ['name' => 'asset_code', 'label' => 'Asset Code', 'type' => 'text', 'required' => true],
                ['name' => 'asset_name', 'label' => 'Asset Name', 'type' => 'text', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'purchase_date', 'label' => 'Purchase Date', 'type' => 'date'],
                ['name' => 'purchase_cost', 'label' => 'Purchase Cost', 'type' => 'number'],
                ['name' => 'useful_life_months', 'label' => 'Useful Life (Months)', 'type' => 'number'],
                ['name' => 'salvage_value', 'label' => 'Salvage Value', 'type' => 'number'],
                ['name' => 'depreciation_method', 'label' => 'Method', 'type' => 'select', 'options' => [
                    'straight_line' => 'Straight Line',
                    'declining' => 'Declining',
                    'units' => 'Units of Production',
                ]],
                ['name' => 'auto_depreciation_enabled', 'label' => 'Enable Auto Depreciation', 'type' => 'checkbox'],
                ['name' => 'depreciation_start_date', 'label' => 'Depreciation Start Date', 'type' => 'date'],
                ['name' => 'depreciation_months', 'label' => 'Depreciation Duration (Months)', 'type' => 'number'],
                ['name' => 'depreciation_expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'depreciation_accumulated_account_code', 'label' => 'Accumulated Depreciation Account', 'type' => 'select', 'lookup' => 'asset_account_code'],
                ['name' => 'accumulated_depreciation', 'label' => 'Accumulated Depreciation', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'active' => 'Active',
                    'disposed' => 'Disposed',
                    'under_maintenance' => 'Under Maintenance',
                ]],
            ],
            'list_columns' => ['asset_code', 'asset_name', 'cost_center_code', 'purchase_cost', 'auto_depreciation_enabled', 'depreciation_start_date', 'depreciation_months', 'accumulated_depreciation', 'status'],
            'search_fields' => ['asset_code', 'asset_name', 'status'],
        ],
        'depreciation_runs' => [
            'title' => 'Automatic Entries',
            'icon' => '*',
            'table' => 'depreciation_runs',
            'description' => 'Run monthly fixed-asset depreciation and auto-post journal entries.',
            'fields' => [
                ['name' => 'run_number', 'label' => 'Run Number', 'type' => 'text', 'required' => true],
                ['name' => 'run_date', 'label' => 'Run Date', 'type' => 'date', 'required' => true],
                ['name' => 'period_month', 'label' => 'Period Month', 'type' => 'number', 'required' => true],
                ['name' => 'period_year', 'label' => 'Period Year', 'type' => 'number', 'required' => true],
                ['name' => 'asset_id', 'label' => 'Asset to Depreciate', 'type' => 'select', 'lookup' => 'asset_id'],
                ['name' => 'asset_code', 'label' => 'Asset Code', 'type' => 'text'],
                ['name' => 'schedule_start_date', 'label' => 'Depreciation Start Date', 'type' => 'date'],
                ['name' => 'duration_months', 'label' => 'Depreciation Duration (Months)', 'type' => 'number'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'accumulated_account_code', 'label' => 'Accumulated Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'total_amount', 'label' => 'Total Amount', 'type' => 'number'],
                ['name' => 'assets_count', 'label' => 'Assets Count', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['run_number', 'run_date', 'period_month', 'period_year', 'asset_code', 'cost_center_code', 'total_amount', 'assets_count', 'status'],
            'search_fields' => ['run_number', 'asset_code', 'cost_center_code', 'status', 'notes'],
        ],
        'ifrs16_leases' => [
            'title' => 'IFRS16 Leases',
            'icon' => 'L16',
            'table' => 'ifrs16_leases',
            'description' => 'Calculate IFRS 16 right-of-use assets, lease liabilities, interest, depreciation, and draft journal entries.',
            'fields' => [
                ['name' => 'lease_number', 'label' => 'Lease Number', 'type' => 'text', 'required' => true],
                ['name' => 'lease_name', 'label' => 'Lease Name', 'type' => 'text', 'required' => true],
                ['name' => 'lessor_code', 'label' => 'Lessor / Vendor', 'type' => 'select', 'lookup' => 'vendor_contact_code'],
                ['name' => 'country', 'label' => 'Country', 'type' => 'text'],
                ['name' => 'currency_code', 'label' => 'Currency', 'type' => 'text'],
                ['name' => 'commencement_date', 'label' => 'Commencement Date', 'type' => 'date', 'required' => true],
                ['name' => 'end_date', 'label' => 'End Date', 'type' => 'date', 'required' => true],
                ['name' => 'first_payment_date', 'label' => 'First Payment Date', 'type' => 'date', 'required' => true],
                ['name' => 'payment_frequency', 'label' => 'Payment Frequency', 'type' => 'select', 'required' => true, 'options' => [
                    'monthly' => 'Monthly',
                    'quarterly' => 'Quarterly',
                    'semiannual' => 'Semiannual',
                    'annual' => 'Annual',
                ]],
                ['name' => 'payment_amount', 'label' => 'Default Payment Amount', 'type' => 'number'],
                ['name' => 'number_of_payments', 'label' => 'Number of Payments (0 = until end date)', 'type' => 'number'],
                ['name' => 'payments_at_or_before_commencement', 'label' => 'Payments At/Before Commencement', 'type' => 'number'],
                ['name' => 'initial_direct_costs', 'label' => 'Initial Direct Costs', 'type' => 'number'],
                ['name' => 'restoration_cost', 'label' => 'Restoration / Dismantling Cost', 'type' => 'number'],
                ['name' => 'lease_incentives', 'label' => 'Lease Incentives Received', 'type' => 'number'],
                ['name' => 'benchmark_rate_percent', 'label' => 'Benchmark Rate %', 'type' => 'number'],
                ['name' => 'credit_spread_percent', 'label' => 'Credit Spread %', 'type' => 'number'],
                ['name' => 'manual_discount_rate_percent', 'label' => 'Manual Discount Rate Override %', 'type' => 'number'],
                ['name' => 'discount_rate_percent', 'label' => 'IBR Used %', 'type' => 'number'],
                ['name' => 'lease_term_days', 'label' => 'Lease Term Days', 'type' => 'number'],
                ['name' => 'lease_term_months', 'label' => 'Lease Term Months', 'type' => 'number'],
                ['name' => 'pv_lease_liability', 'label' => 'PV Lease Liability', 'type' => 'number'],
                ['name' => 'initial_rou_asset', 'label' => 'Initial ROU Asset', 'type' => 'number'],
                ['name' => 'daily_depreciation', 'label' => 'Daily Depreciation', 'type' => 'number'],
                ['name' => 'monthly_depreciation', 'label' => 'Monthly Depreciation Approx.', 'type' => 'number'],
                ['name' => 'total_interest', 'label' => 'Total Interest', 'type' => 'number'],
                ['name' => 'closing_liability_check', 'label' => 'Closing Liability Check', 'type' => 'number'],
                ['name' => 'rou_asset_account_code', 'label' => 'ROU Asset Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'lease_liability_account_code', 'label' => 'Lease Liability Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'interest_expense_account_code', 'label' => 'Interest Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'depreciation_expense_account_code', 'label' => 'Depreciation Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'accumulated_depreciation_account_code', 'label' => 'Accumulated Depreciation Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'restoration_provision_account_code', 'label' => 'Restoration Provision Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'lease_incentive_account_code', 'label' => 'Lease Incentive Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'active' => 'Active',
                    'closed' => 'Closed',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['lease_number', 'lease_name', 'commencement_date', 'end_date', 'payment_amount', 'number_of_payments', 'discount_rate_percent', 'pv_lease_liability', 'initial_rou_asset', 'closing_liability_check', 'status'],
            'search_fields' => ['lease_number', 'lease_name', 'lessor_code', 'country', 'status', 'notes'],
        ],
        'ifrs16_lease_schedules' => [
            'title' => 'IFRS16 Lease Schedule',
            'icon' => 'LS',
            'table' => 'ifrs16_lease_schedules',
            'description' => 'Generated IFRS 16 payment-date schedule and recurring draft journals.',
            'fields' => [
                ['name' => 'lease_id', 'label' => 'Lease', 'type' => 'select', 'lookup' => 'ifrs16_lease_id', 'required' => true],
                ['name' => 'lease_number', 'label' => 'Lease Number', 'type' => 'text'],
                ['name' => 'sequence_no', 'label' => 'Sequence No', 'type' => 'number'],
                ['name' => 'schedule_date', 'label' => 'Payment Date', 'type' => 'date', 'required' => true],
                ['name' => 'opening_liability', 'label' => 'Opening Liability', 'type' => 'number'],
                ['name' => 'days_count', 'label' => 'Days', 'type' => 'number'],
                ['name' => 'interest_expense', 'label' => 'Interest Expense', 'type' => 'number'],
                ['name' => 'payment_amount', 'label' => 'Payment', 'type' => 'number'],
                ['name' => 'liability_reduction', 'label' => 'Liability Reduction', 'type' => 'number'],
                ['name' => 'closing_liability', 'label' => 'Closing Liability', 'type' => 'number'],
                ['name' => 'depreciation_expense', 'label' => 'Depreciation Expense', 'type' => 'number'],
                ['name' => 'accumulated_depreciation', 'label' => 'Accumulated Depreciation', 'type' => 'number'],
                ['name' => 'rou_asset_nbv', 'label' => 'ROU Asset NBV', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['lease_number', 'sequence_no', 'schedule_date', 'opening_liability', 'interest_expense', 'payment_amount', 'liability_reduction', 'closing_liability', 'depreciation_expense', 'rou_asset_nbv', 'status'],
            'search_fields' => ['lease_number', 'status', 'notes'],
        ],
        'hr_departments' => [
            'title' => 'HR Departments',
            'icon' => 'HD',
            'table' => 'hr_departments',
            'description' => 'Human resources department structure and cost center mapping.',
            'fields' => [
                ['name' => 'department_code', 'label' => 'Department Code', 'type' => 'text', 'required' => true],
                ['name' => 'department_name', 'label' => 'Department Name', 'type' => 'text', 'required' => true],
                ['name' => 'manager_name', 'label' => 'Manager', 'type' => 'text'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['department_code', 'department_name', 'manager_name', 'cost_center_code', 'is_active'],
            'search_fields' => ['department_code', 'department_name', 'manager_name'],
        ],
        'hr_job_titles' => [
            'title' => 'Job Titles',
            'icon' => 'JT',
            'table' => 'hr_job_titles',
            'description' => 'Job titles, grades, and department assignment.',
            'fields' => [
                ['name' => 'job_code', 'label' => 'Job Code', 'type' => 'text', 'required' => true],
                ['name' => 'job_title', 'label' => 'Job Title', 'type' => 'text', 'required' => true],
                ['name' => 'department_code', 'label' => 'Department', 'type' => 'select', 'lookup' => 'hr_department_code'],
                ['name' => 'grade_name', 'label' => 'Grade', 'type' => 'text'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['job_code', 'job_title', 'department_code', 'grade_name', 'is_active'],
            'search_fields' => ['job_code', 'job_title', 'department_code', 'grade_name'],
        ],
        'hr_shifts' => [
            'title' => 'Work Shifts',
            'icon' => 'SH',
            'table' => 'hr_shifts',
            'description' => 'Define working schedules used by attendance records.',
            'fields' => [
                ['name' => 'shift_code', 'label' => 'Shift Code', 'type' => 'text', 'required' => true],
                ['name' => 'shift_name', 'label' => 'Shift Name', 'type' => 'text', 'required' => true],
                ['name' => 'start_time', 'label' => 'Start Time', 'type' => 'text'],
                ['name' => 'end_time', 'label' => 'End Time', 'type' => 'text'],
                ['name' => 'working_hours', 'label' => 'Working Hours', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['shift_code', 'shift_name', 'start_time', 'end_time', 'working_hours', 'is_active'],
            'search_fields' => ['shift_code', 'shift_name'],
        ],
        'hr_leave_types' => [
            'title' => 'Leave Types',
            'icon' => 'LT',
            'table' => 'hr_leave_types',
            'description' => 'Define paid and unpaid leave policies and balances.',
            'fields' => [
                ['name' => 'leave_type_code', 'label' => 'Leave Type Code', 'type' => 'text', 'required' => true],
                ['name' => 'leave_type_name', 'label' => 'Leave Type Name', 'type' => 'text', 'required' => true],
                ['name' => 'annual_balance_days', 'label' => 'Annual Balance Days', 'type' => 'number'],
                ['name' => 'is_paid', 'label' => 'Paid Leave', 'type' => 'checkbox'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['leave_type_code', 'leave_type_name', 'annual_balance_days', 'is_paid', 'is_active'],
            'search_fields' => ['leave_type_code', 'leave_type_name'],
        ],
        'hr_employee_contracts' => [
            'title' => 'Employee Contracts',
            'icon' => 'CT',
            'table' => 'hr_employee_contracts',
            'description' => 'Track employee contracts, salaries, departments, and job assignments.',
            'fields' => [
                ['name' => 'contract_number', 'label' => 'Contract Number', 'type' => 'text', 'required' => true],
                ['name' => 'employee_number', 'label' => 'Employee', 'type' => 'select', 'lookup' => 'employee_number', 'required' => true],
                ['name' => 'department_code', 'label' => 'Department', 'type' => 'select', 'lookup' => 'hr_department_code'],
                ['name' => 'job_code', 'label' => 'Job Title', 'type' => 'select', 'lookup' => 'hr_job_code'],
                ['name' => 'contract_type', 'label' => 'Contract Type', 'type' => 'select', 'options' => [
                    'full_time' => 'Full Time',
                    'part_time' => 'Part Time',
                    'temporary' => 'Temporary',
                    'consultant' => 'Consultant',
                ]],
                ['name' => 'start_date', 'label' => 'Start Date', 'type' => 'date', 'required' => true],
                ['name' => 'end_date', 'label' => 'End Date', 'type' => 'date'],
                ['name' => 'basic_salary', 'label' => 'Basic Salary', 'type' => 'number'],
                ['name' => 'allowance', 'label' => 'Allowance', 'type' => 'number'],
                ['name' => 'insurance_number', 'label' => 'Insurance Number', 'type' => 'text'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'active' => 'Active',
                    'expired' => 'Expired',
                    'terminated' => 'Terminated',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['contract_number', 'employee_number', 'department_code', 'job_code', 'contract_type', 'start_date', 'end_date', 'basic_salary', 'status'],
            'search_fields' => ['contract_number', 'employee_number', 'department_code', 'job_code', 'status'],
        ],
        'hr_salary_components' => [
            'title' => 'Salary Components',
            'icon' => 'SC',
            'table' => 'hr_salary_components',
            'description' => 'Reusable earnings, allowances, deductions, and payroll posting accounts.',
            'fields' => [
                ['name' => 'component_code', 'label' => 'Component Code', 'type' => 'text', 'required' => true],
                ['name' => 'component_name', 'label' => 'Component Name', 'type' => 'text', 'required' => true],
                ['name' => 'component_type', 'label' => 'Type', 'type' => 'select', 'options' => [
                    'earning' => 'Earning',
                    'allowance' => 'Allowance',
                    'deduction' => 'Deduction',
                    'employer_cost' => 'Employer Cost',
                ]],
                ['name' => 'calculation_type', 'label' => 'Calculation', 'type' => 'select', 'options' => [
                    'fixed' => 'Fixed',
                    'percent' => 'Percent',
                    'manual' => 'Manual',
                ]],
                ['name' => 'default_amount', 'label' => 'Default Amount', 'type' => 'number'],
                ['name' => 'percent_value', 'label' => 'Percent %', 'type' => 'number'],
                ['name' => 'account_code', 'label' => 'Posting Account', 'type' => 'select', 'lookup' => 'chart_account_code'],
                ['name' => 'is_taxable', 'label' => 'Taxable', 'type' => 'checkbox'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['component_code', 'component_name', 'component_type', 'calculation_type', 'default_amount', 'percent_value', 'account_code', 'is_active'],
            'search_fields' => ['component_code', 'component_name', 'component_type', 'account_code'],
        ],
        'employees' => [
            'title' => 'Employees',
            'icon' => '*',
            'table' => 'employees',
            'description' => 'Employee master data for payroll.',
            'fields' => [
                ['name' => 'employee_number', 'label' => 'Employee No', 'type' => 'text', 'required' => true],
                ['name' => 'full_name', 'label' => 'Full Name', 'type' => 'text', 'required' => true],
                ['name' => 'department_code', 'label' => 'Department', 'type' => 'select', 'lookup' => 'hr_department_code'],
                ['name' => 'job_code', 'label' => 'Job Title', 'type' => 'select', 'lookup' => 'hr_job_code'],
                ['name' => 'department', 'label' => 'Department', 'type' => 'text'],
                ['name' => 'basic_salary', 'label' => 'Basic Salary', 'type' => 'number'],
                ['name' => 'allowance', 'label' => 'Allowance', 'type' => 'number'],
                ['name' => 'deduction_default', 'label' => 'Default Deduction', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['employee_number', 'full_name', 'department_code', 'job_code', 'department', 'basic_salary', 'is_active'],
            'search_fields' => ['employee_number', 'full_name', 'department_code', 'job_code', 'department'],
        ],
        'hr_attendance' => [
            'title' => 'Attendance',
            'icon' => 'AT',
            'table' => 'hr_attendance',
            'description' => 'Daily attendance, lateness, absence, and hours worked.',
            'fields' => [
                ['name' => 'attendance_number', 'label' => 'Attendance Number', 'type' => 'text', 'required' => true],
                ['name' => 'employee_number', 'label' => 'Employee', 'type' => 'select', 'lookup' => 'employee_number', 'required' => true],
                ['name' => 'shift_code', 'label' => 'Shift', 'type' => 'select', 'lookup' => 'hr_shift_code'],
                ['name' => 'attendance_date', 'label' => 'Attendance Date', 'type' => 'date', 'required' => true],
                ['name' => 'check_in', 'label' => 'Check In', 'type' => 'text'],
                ['name' => 'check_out', 'label' => 'Check Out', 'type' => 'text'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'present' => 'Present',
                    'late' => 'Late',
                    'absent' => 'Absent',
                    'leave' => 'Leave',
                    'holiday' => 'Holiday',
                ]],
                ['name' => 'hours_worked', 'label' => 'Hours Worked', 'type' => 'number'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['attendance_number', 'attendance_date', 'employee_number', 'shift_code', 'status', 'hours_worked'],
            'search_fields' => ['attendance_number', 'employee_number', 'shift_code', 'status'],
        ],
        'hr_leave_requests' => [
            'title' => 'Leave Requests',
            'icon' => 'LV',
            'table' => 'hr_leave_requests',
            'description' => 'Employee leave requests and approval status.',
            'fields' => [
                ['name' => 'request_number', 'label' => 'Request Number', 'type' => 'text', 'required' => true],
                ['name' => 'employee_number', 'label' => 'Employee', 'type' => 'select', 'lookup' => 'employee_number', 'required' => true],
                ['name' => 'leave_type_code', 'label' => 'Leave Type', 'type' => 'select', 'lookup' => 'hr_leave_type_code'],
                ['name' => 'leave_type', 'label' => 'Leave Type', 'type' => 'select', 'options' => [
                    'annual' => 'Annual',
                    'sick' => 'Sick',
                    'emergency' => 'Emergency',
                    'unpaid' => 'Unpaid',
                ]],
                ['name' => 'start_date', 'label' => 'Start Date', 'type' => 'date', 'required' => true],
                ['name' => 'end_date', 'label' => 'End Date', 'type' => 'date', 'required' => true],
                ['name' => 'days_count', 'label' => 'Days Count', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'pending' => 'Pending',
                    'approved' => 'Approved',
                    'rejected' => 'Rejected',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['request_number', 'employee_number', 'leave_type_code', 'leave_type', 'start_date', 'end_date', 'days_count', 'status'],
            'search_fields' => ['request_number', 'employee_number', 'leave_type_code', 'leave_type', 'status'],
        ],
        'payroll_runs' => [
            'title' => 'Payroll Runs',
            'icon' => '*',
            'table' => 'payroll_runs',
            'description' => 'Payroll cycle totals and status.',
            'fields' => [
                ['name' => 'run_number', 'label' => 'Run Number', 'type' => 'text', 'required' => true],
                ['name' => 'period_month', 'label' => 'Month', 'type' => 'number', 'required' => true],
                ['name' => 'period_year', 'label' => 'Year', 'type' => 'number', 'required' => true],
                ['name' => 'total_gross', 'label' => 'Total Gross', 'type' => 'number'],
                ['name' => 'total_deductions', 'label' => 'Total Deductions', 'type' => 'number'],
                ['name' => 'total_net', 'label' => 'Total Net', 'type' => 'number'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'approved' => 'Approved',
                    'posted' => 'Posted',
                ]],
            ],
            'list_columns' => ['run_number', 'period_month', 'period_year', 'total_net', 'status'],
            'search_fields' => ['run_number', 'status'],
        ],
        'hr_payroll_items' => [
            'title' => 'Payroll Items',
            'icon' => 'PI',
            'table' => 'hr_payroll_items',
            'description' => 'Detailed payroll lines by employee and salary component.',
            'fields' => [
                ['name' => 'item_number', 'label' => 'Item Number', 'type' => 'text', 'required' => true],
                ['name' => 'run_number', 'label' => 'Payroll Run', 'type' => 'select', 'lookup' => 'payroll_run_number', 'required' => true],
                ['name' => 'employee_number', 'label' => 'Employee', 'type' => 'select', 'lookup' => 'employee_number', 'required' => true],
                ['name' => 'component_code', 'label' => 'Salary Component', 'type' => 'select', 'lookup' => 'hr_salary_component_code'],
                ['name' => 'component_type', 'label' => 'Type', 'type' => 'select', 'options' => [
                    'earning' => 'Earning',
                    'allowance' => 'Allowance',
                    'deduction' => 'Deduction',
                    'employer_cost' => 'Employer Cost',
                ]],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'account_code', 'label' => 'Posting Account', 'type' => 'select', 'lookup' => 'chart_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['item_number', 'run_number', 'employee_number', 'component_code', 'component_type', 'amount', 'account_code', 'cost_center_code'],
            'search_fields' => ['item_number', 'run_number', 'employee_number', 'component_code', 'component_type'],
        ],
        'inventory_movements' => [
            'title' => 'Inventory Movements',
            'icon' => '*',
            'table' => 'inventory_movements',
            'description' => 'Stock in/out movements and valuation.',
            'fields' => [
                ['name' => 'movement_number', 'label' => 'Movement Number', 'type' => 'text', 'required' => true],
                ['name' => 'movement_date', 'label' => 'Movement Date', 'type' => 'date', 'required' => true],
                ['name' => 'movement_type', 'label' => 'Type', 'type' => 'select', 'options' => [
                    'in' => 'In',
                    'out' => 'Out',
                    'adjustment' => 'Adjustment',
                ]],
                ['name' => 'sku', 'label' => 'SKU', 'type' => 'select', 'lookup' => 'product_sku'],
                ['name' => 'quantity', 'label' => 'Quantity', 'type' => 'number', 'required' => true],
                ['name' => 'unit_cost', 'label' => 'Unit Cost', 'type' => 'number'],
                ['name' => 'reference', 'label' => 'Reference', 'type' => 'text'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['movement_number', 'movement_date', 'movement_type', 'sku', 'quantity', 'unit_cost'],
            'search_fields' => ['movement_number', 'sku', 'reference'],
        ],
        'student_stages' => [
            'title' => 'Student Stages',
            'icon' => 'ST',
            'table' => 'student_stages',
            'description' => 'Saudi school stages such as KG, Primary, Intermediate, and Secondary.',
            'fields' => [
                ['name' => 'stage_code', 'label' => 'Stage Code', 'type' => 'text', 'required' => true],
                ['name' => 'stage_name', 'label' => 'Stage Name', 'type' => 'text', 'required' => true],
                ['name' => 'sort_order', 'label' => 'Sort Order', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['stage_code', 'stage_name', 'sort_order', 'is_active'],
            'search_fields' => ['stage_code', 'stage_name'],
        ],
        'student_grades' => [
            'title' => 'Student Grades',
            'icon' => 'GR',
            'table' => 'student_grades',
            'description' => 'Grades are linked to one stage, preventing grade 12 from appearing under KG.',
            'fields' => [
                ['name' => 'grade_code', 'label' => 'Grade Code', 'type' => 'text', 'required' => true],
                ['name' => 'grade_name', 'label' => 'Grade Name', 'type' => 'text', 'required' => true],
                ['name' => 'stage_code', 'label' => 'Stage', 'type' => 'select', 'lookup' => 'student_stage_code', 'required' => true],
                ['name' => 'sort_order', 'label' => 'Sort Order', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['grade_code', 'grade_name', 'stage_code', 'sort_order', 'is_active'],
            'search_fields' => ['grade_code', 'grade_name', 'stage_code'],
        ],
        'student_classes' => [
            'title' => 'Student Classes',
            'icon' => 'CL',
            'table' => 'student_classes',
            'description' => 'Class/section definitions linked to academic year, stage, and grade.',
            'fields' => [
                ['name' => 'class_code', 'label' => 'Class Code', 'type' => 'text', 'required' => true],
                ['name' => 'class_name', 'label' => 'Class Name', 'type' => 'text', 'required' => true],
                ['name' => 'academic_year', 'label' => 'Academic Year', 'type' => 'text', 'required' => true],
                ['name' => 'stage_code', 'label' => 'Stage', 'type' => 'select', 'lookup' => 'student_stage_code', 'required' => true],
                ['name' => 'grade_code', 'label' => 'Grade', 'type' => 'select', 'lookup' => 'student_grade_code', 'required' => true],
                ['name' => 'capacity', 'label' => 'Capacity', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['class_code', 'class_name', 'academic_year', 'stage_code', 'grade_code', 'capacity', 'is_active'],
            'search_fields' => ['class_code', 'class_name', 'academic_year', 'stage_code', 'grade_code'],
        ],
        'students' => [
            'title' => 'Students',
            'icon' => 'SD',
            'table' => 'students',
            'description' => 'Student master data, guardian contact, stage, grade, and class.',
            'fields' => [
                ['name' => 'student_number', 'label' => 'Student Number', 'type' => 'text', 'required' => true],
                ['name' => 'student_name', 'label' => 'Student Name', 'type' => 'text', 'required' => true],
                ['name' => 'national_id', 'label' => 'National ID / Iqama', 'type' => 'text'],
                ['name' => 'guardian_contact_code', 'label' => 'Guardian / Customer', 'type' => 'select', 'lookup' => 'customer_contact_code', 'required' => true],
                ['name' => 'stage_code', 'label' => 'Stage', 'type' => 'select', 'lookup' => 'student_stage_code', 'required' => true],
                ['name' => 'grade_code', 'label' => 'Grade', 'type' => 'select', 'lookup' => 'student_grade_code', 'required' => true],
                ['name' => 'class_code', 'label' => 'Class', 'type' => 'select', 'lookup' => 'student_class_code'],
                ['name' => 'enrollment_date', 'label' => 'Enrollment Date', 'type' => 'date'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'active' => 'Active',
                    'suspended' => 'Suspended',
                    'graduated' => 'Graduated',
                    'withdrawn' => 'Withdrawn',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['student_number', 'student_name', 'guardian_contact_code', 'stage_code', 'grade_code', 'class_code', 'status'],
            'search_fields' => ['student_number', 'student_name', 'national_id', 'guardian_contact_code', 'stage_code', 'grade_code', 'class_code', 'status'],
        ],
        'student_revenue_types' => [
            'title' => 'Student Revenue Types',
            'icon' => 'RT',
            'table' => 'student_revenue_types',
            'description' => 'Tuition, registration, transport, books, uniforms, activities, exams, and other student revenues.',
            'fields' => [
                ['name' => 'revenue_type_code', 'label' => 'Revenue Type Code', 'type' => 'text', 'required' => true],
                ['name' => 'revenue_type_name', 'label' => 'Revenue Type Name', 'type' => 'text', 'required' => true],
                ['name' => 'revenue_category', 'label' => 'Category', 'type' => 'select', 'options' => [
                    'tuition' => 'Tuition',
                    'registration' => 'Registration',
                    'transport' => 'Transport',
                    'books' => 'Books',
                    'uniform' => 'Uniform',
                    'activities' => 'Activities',
                    'exam' => 'Exam',
                    'cafeteria' => 'Cafeteria',
                    'other' => 'Other',
                ]],
                ['name' => 'default_amount', 'label' => 'Default Amount', 'type' => 'number'],
                ['name' => 'is_taxable', 'label' => 'Taxable', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'sales_tax_name'],
                ['name' => 'revenue_account_code', 'label' => 'Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['revenue_type_code', 'revenue_type_name', 'revenue_category', 'default_amount', 'is_taxable', 'revenue_account_code', 'cost_center_code', 'is_active'],
            'search_fields' => ['revenue_type_code', 'revenue_type_name', 'revenue_category', 'revenue_account_code'],
        ],
        'student_fee_plans' => [
            'title' => 'Student Fee Plans',
            'icon' => 'FP',
            'table' => 'student_fee_plans',
            'description' => 'Stage/grade fee plans with installment settings and revenue type mapping.',
            'fields' => [
                ['name' => 'fee_plan_code', 'label' => 'Fee Plan Code', 'type' => 'text', 'required' => true],
                ['name' => 'fee_plan_name', 'label' => 'Fee Plan Name', 'type' => 'text', 'required' => true],
                ['name' => 'academic_year', 'label' => 'Academic Year', 'type' => 'text', 'required' => true],
                ['name' => 'stage_code', 'label' => 'Stage', 'type' => 'select', 'lookup' => 'student_stage_code', 'required' => true],
                ['name' => 'grade_code', 'label' => 'Grade', 'type' => 'select', 'lookup' => 'student_grade_code', 'required' => true],
                ['name' => 'revenue_type_code', 'label' => 'Revenue Type', 'type' => 'select', 'lookup' => 'student_revenue_type_code', 'required' => true],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'sales_tax_name'],
                ['name' => 'installments_count', 'label' => 'Installments Count', 'type' => 'number'],
                ['name' => 'due_day', 'label' => 'Due Day', 'type' => 'number'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['fee_plan_code', 'fee_plan_name', 'academic_year', 'stage_code', 'grade_code', 'revenue_type_code', 'amount', 'installments_count', 'is_active'],
            'search_fields' => ['fee_plan_code', 'fee_plan_name', 'academic_year', 'stage_code', 'grade_code', 'revenue_type_code'],
        ],
        'student_account_settings' => [
            'title' => 'Student GL Settings',
            'icon' => 'GL',
            'table' => 'student_account_settings',
            'description' => 'Map student receivables, advances, refunds, default revenue, and cost center to General Ledger.',
            'fields' => [
                ['name' => 'receivable_account_code', 'label' => 'Student Receivable Account', 'type' => 'select', 'lookup' => 'asset_account_code', 'required' => true],
                ['name' => 'advance_account_code', 'label' => 'Student Advance / Unearned Account', 'type' => 'select', 'lookup' => 'liability_account_code', 'required' => true],
                ['name' => 'discount_account_code', 'label' => 'Refund / Discount Offset Account', 'type' => 'select', 'lookup' => 'chart_account_code', 'required' => true],
                ['name' => 'default_revenue_account_code', 'label' => 'Default Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code', 'required' => true],
                ['name' => 'default_cost_center_code', 'label' => 'Default Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['receivable_account_code', 'advance_account_code', 'discount_account_code', 'default_revenue_account_code', 'default_cost_center_code'],
            'search_fields' => ['receivable_account_code', 'advance_account_code', 'discount_account_code', 'default_revenue_account_code', 'default_cost_center_code'],
        ],
        'student_invoices' => [
            'title' => 'Student Invoices',
            'icon' => 'SI',
            'table' => 'student_invoices',
            'description' => 'Student billing with GL posting, collection tracking, tax, and ZATCA QR print.',
            'fields' => [
                ['name' => 'student_invoice_number', 'label' => 'Invoice Number', 'type' => 'text', 'required' => true],
                ['name' => 'invoice_date', 'label' => 'Invoice Date', 'type' => 'date', 'required' => true],
                ['name' => 'academic_year', 'label' => 'Academic Year', 'type' => 'text', 'required' => true],
                ['name' => 'student_number', 'label' => 'Student', 'type' => 'select', 'lookup' => 'student_number', 'required' => true],
                ['name' => 'guardian_contact_code', 'label' => 'Guardian / Customer', 'type' => 'select', 'lookup' => 'customer_contact_code', 'required' => true],
                ['name' => 'stage_code', 'label' => 'Stage', 'type' => 'select', 'lookup' => 'student_stage_code', 'required' => true],
                ['name' => 'grade_code', 'label' => 'Grade', 'type' => 'select', 'lookup' => 'student_grade_code', 'required' => true],
                ['name' => 'class_code', 'label' => 'Class', 'type' => 'select', 'lookup' => 'student_class_code'],
                ['name' => 'revenue_type_code', 'label' => 'Revenue Type', 'type' => 'select', 'lookup' => 'student_revenue_type_code', 'required' => true],
                ['name' => 'description', 'label' => 'Description', 'type' => 'text'],
                ['name' => 'revenue_account_code', 'label' => 'Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'is_taxable', 'label' => 'Taxable Invoice', 'type' => 'checkbox'],
                ['name' => 'tax_name', 'label' => 'Tax Code', 'type' => 'select', 'lookup' => 'sales_tax_name'],
                ['name' => 'tax_rate_percent', 'label' => 'Tax Rate %', 'type' => 'number'],
                ['name' => 'subtotal', 'label' => 'Subtotal', 'type' => 'number', 'required' => true],
                ['name' => 'discount_total', 'label' => 'Discount', 'type' => 'number'],
                ['name' => 'tax_total', 'label' => 'Tax', 'type' => 'number'],
                ['name' => 'grand_total', 'label' => 'Grand Total', 'type' => 'number', 'required' => true],
                ['name' => 'inline_paid_amount', 'label' => 'Collected In Entry', 'type' => 'number'],
                ['name' => 'payment_method', 'label' => 'Collection Method', 'type' => 'select', 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Collected In', 'type' => 'select', 'lookup' => 'payment_account_name'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'issued' => 'Issued',
                    'partially_paid' => 'Partially Paid',
                    'paid' => 'Paid',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['student_invoice_number', 'invoice_date', 'student_number', 'guardian_contact_code', 'stage_code', 'grade_code', 'revenue_type_code', 'grand_total', 'paid_amount', 'status'],
            'search_fields' => ['student_invoice_number', 'academic_year', 'student_number', 'guardian_contact_code', 'stage_code', 'grade_code', 'revenue_type_code', 'status'],
        ],
        'student_receipts' => [
            'title' => 'Student Receipts',
            'icon' => 'RC',
            'table' => 'student_receipts',
            'description' => 'Receipt vouchers for student invoices or advance collections.',
            'fields' => [
                ['name' => 'receipt_number', 'label' => 'Receipt Number', 'type' => 'text', 'required' => true],
                ['name' => 'receipt_date', 'label' => 'Receipt Date', 'type' => 'date', 'required' => true],
                ['name' => 'student_invoice_id', 'label' => 'Student Invoice', 'type' => 'select', 'lookup' => 'student_invoice_id'],
                ['name' => 'student_number', 'label' => 'Student', 'type' => 'select', 'lookup' => 'student_number', 'required' => true],
                ['name' => 'guardian_contact_code', 'label' => 'Guardian / Customer', 'type' => 'select', 'lookup' => 'customer_contact_code'],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Collected In', 'type' => 'select', 'lookup' => 'payment_account_name', 'required' => true],
                ['name' => 'reference', 'label' => 'Reference', 'type' => 'text'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['receipt_number', 'receipt_date', 'student_invoice_id', 'student_number', 'guardian_contact_code', 'amount', 'payment_method', 'payment_account_name'],
            'search_fields' => ['receipt_number', 'reference', 'student_number', 'guardian_contact_code', 'payment_method'],
        ],
        'student_refunds' => [
            'title' => 'Student Payment Vouchers',
            'icon' => 'PV',
            'table' => 'student_refunds',
            'description' => 'Payment vouchers/refunds to students or guardians with GL offset account.',
            'fields' => [
                ['name' => 'voucher_number', 'label' => 'Voucher Number', 'type' => 'text', 'required' => true],
                ['name' => 'voucher_date', 'label' => 'Voucher Date', 'type' => 'date', 'required' => true],
                ['name' => 'student_number', 'label' => 'Student', 'type' => 'select', 'lookup' => 'student_number', 'required' => true],
                ['name' => 'guardian_contact_code', 'label' => 'Guardian / Customer', 'type' => 'select', 'lookup' => 'customer_contact_code'],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name', 'required' => true],
                ['name' => 'offset_account_code', 'label' => 'Offset Account', 'type' => 'select', 'lookup' => 'chart_account_code', 'required' => true],
                ['name' => 'reason', 'label' => 'Reason', 'type' => 'text'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['voucher_number', 'voucher_date', 'student_number', 'guardian_contact_code', 'amount', 'payment_method', 'payment_account_name', 'offset_account_code'],
            'search_fields' => ['voucher_number', 'student_number', 'guardian_contact_code', 'reason', 'payment_method'],
        ],
        'excellence_hospitals' => [
            'title' => 'EXcellence Hospitals',
            'icon' => 'EH',
            'table' => 'excellence_hospitals',
            'description' => 'Hospitals served by EXcellence medical device rental operations.',
            'fields' => [
                ['name' => 'hospital_code', 'label' => 'Hospital Code', 'type' => 'text', 'required' => true],
                ['name' => 'hospital_name', 'label' => 'Hospital Name', 'type' => 'text', 'required' => true],
                ['name' => 'tax_number', 'label' => 'Tax Number', 'type' => 'text'],
                ['name' => 'phone', 'label' => 'Phone', 'type' => 'text'],
                ['name' => 'email', 'label' => 'Email', 'type' => 'text'],
                ['name' => 'address', 'label' => 'Address', 'type' => 'textarea'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['hospital_code', 'hospital_name', 'tax_number', 'phone', 'is_active'],
            'search_fields' => ['hospital_code', 'hospital_name', 'tax_number', 'phone', 'email'],
        ],
        'excellence_devices' => [
            'title' => 'Medical Devices',
            'icon' => 'MD',
            'table' => 'excellence_devices',
            'description' => 'Medical devices available for hospital procedure rentals with default GL mapping.',
            'fields' => [
                ['name' => 'device_code', 'label' => 'Device Code', 'type' => 'text', 'required' => true],
                ['name' => 'device_name', 'label' => 'Device Name', 'type' => 'text', 'required' => true],
                ['name' => 'serial_number', 'label' => 'Serial Number', 'type' => 'text'],
                ['name' => 'category', 'label' => 'Category', 'type' => 'select', 'options' => [
                    'surgical' => 'Surgical Equipment',
                    'laparoscopy' => 'Laparoscopy',
                    'orthopedic' => 'Orthopedic',
                    'imaging' => 'Imaging',
                    'monitoring' => 'Monitoring',
                    'other' => 'Other',
                ]],
                ['name' => 'default_rental_rate', 'label' => 'Default Rental Rate', 'type' => 'number'],
                ['name' => 'revenue_account_code', 'label' => 'Rental Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code'],
                ['name' => 'hospital_expense_account_code', 'label' => 'Hospital Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['device_code', 'device_name', 'serial_number', 'category', 'default_rental_rate', 'revenue_account_code', 'hospital_expense_account_code', 'is_active'],
            'search_fields' => ['device_code', 'device_name', 'serial_number', 'category'],
        ],
        'excellence_operations' => [
            'title' => 'Medical Device Operations',
            'icon' => 'OP',
            'table' => 'excellence_operations',
            'description' => 'Patient procedure rentals: hospital, device, revenue, hospital share/cost, and automatic GL posting.',
            'fields' => [
                ['name' => 'operation_number', 'label' => 'Operation Number', 'type' => 'text', 'required' => true],
                ['name' => 'operation_date', 'label' => 'Operation Date', 'type' => 'date', 'required' => true],
                ['name' => 'hospital_code', 'label' => 'Hospital', 'type' => 'select', 'lookup' => 'excellence_hospital_code', 'required' => true],
                ['name' => 'patient_name', 'label' => 'Patient Name', 'type' => 'text'],
                ['name' => 'patient_file_no', 'label' => 'Patient File No.', 'type' => 'text'],
                ['name' => 'procedure_name', 'label' => 'Procedure Name', 'type' => 'text'],
                ['name' => 'doctor_name', 'label' => 'Doctor Name', 'type' => 'text'],
                ['name' => 'device_code', 'label' => 'Medical Device', 'type' => 'select', 'lookup' => 'excellence_device_code', 'required' => true],
                ['name' => 'quantity', 'label' => 'Quantity', 'type' => 'number'],
                ['name' => 'rental_rate', 'label' => 'Rental Rate', 'type' => 'number'],
                ['name' => 'revenue_amount', 'label' => 'Revenue Amount', 'type' => 'number'],
                ['name' => 'hospital_expense_amount', 'label' => 'Hospital Expense Amount', 'type' => 'number'],
                ['name' => 'net_operation_income', 'label' => 'Net Operation Income', 'type' => 'number'],
                ['name' => 'revenue_account_code', 'label' => 'Revenue Account', 'type' => 'select', 'lookup' => 'revenue_account_code', 'required' => true],
                ['name' => 'hospital_expense_account_code', 'label' => 'Hospital Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'confirmed' => 'Confirmed',
                    'collected' => 'Collected',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['operation_number', 'operation_date', 'hospital_code', 'patient_name', 'device_code', 'revenue_amount', 'hospital_expense_amount', 'net_operation_income', 'status'],
            'search_fields' => ['operation_number', 'hospital_code', 'patient_name', 'patient_file_no', 'procedure_name', 'device_code', 'status'],
        ],
        'excellence_hospital_expenses' => [
            'title' => 'Hospital Expense Payments',
            'icon' => 'HE',
            'table' => 'excellence_hospital_expenses',
            'description' => 'Payments made to hospitals, either settling accrued operation expenses or direct hospital costs.',
            'fields' => [
                ['name' => 'expense_number', 'label' => 'Expense Number', 'type' => 'text', 'required' => true],
                ['name' => 'expense_date', 'label' => 'Expense Date', 'type' => 'date', 'required' => true],
                ['name' => 'hospital_code', 'label' => 'Hospital', 'type' => 'select', 'lookup' => 'excellence_hospital_code', 'required' => true],
                ['name' => 'operation_number', 'label' => 'Related Operation', 'type' => 'select', 'lookup' => 'excellence_operation_number'],
                ['name' => 'settlement_mode', 'label' => 'Settlement Mode', 'type' => 'select', 'options' => [
                    'accrued_operation' => 'Settle Accrued Operation Cost',
                    'direct_expense' => 'Direct Hospital Expense',
                ]],
                ['name' => 'expense_type', 'label' => 'Expense Type', 'type' => 'select', 'options' => [
                    'hospital_share' => 'Hospital Share',
                    'operation_supplies' => 'Operation Supplies',
                    'medical_staff' => 'Medical Staff',
                    'other' => 'Other',
                ]],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name', 'required' => true],
                ['name' => 'offset_account_code', 'label' => 'Accrued Expense Account', 'type' => 'select', 'lookup' => 'liability_account_code'],
                ['name' => 'expense_account_code', 'label' => 'Direct Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code'],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code'],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['expense_number', 'expense_date', 'hospital_code', 'operation_number', 'settlement_mode', 'amount', 'payment_account_name', 'status'],
            'search_fields' => ['expense_number', 'hospital_code', 'operation_number', 'expense_type', 'status'],
        ],
        'excellence_admin_expenses' => [
            'title' => 'EXcellence Administrative Expenses',
            'icon' => 'AE',
            'table' => 'excellence_admin_expenses',
            'description' => 'Administrative expenses outside patient operations, with direct payment posting.',
            'fields' => [
                ['name' => 'expense_number', 'label' => 'Expense Number', 'type' => 'text', 'required' => true],
                ['name' => 'expense_date', 'label' => 'Expense Date', 'type' => 'date', 'required' => true],
                ['name' => 'category', 'label' => 'Category', 'type' => 'select', 'options' => [
                    'administration' => 'Administration',
                    'marketing' => 'Marketing',
                    'maintenance' => 'Maintenance',
                    'transportation' => 'Transportation',
                    'communication' => 'Communication',
                    'other' => 'Other',
                ]],
                ['name' => 'description', 'label' => 'Description', 'type' => 'text'],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name', 'required' => true],
                ['name' => 'expense_account_code', 'label' => 'Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['expense_number', 'expense_date', 'category', 'description', 'amount', 'payment_account_name', 'expense_account_code', 'status'],
            'search_fields' => ['expense_number', 'category', 'description', 'payment_account_name', 'status'],
        ],
        'excellence_salaries' => [
            'title' => 'EXcellence Salaries',
            'icon' => 'ES',
            'table' => 'excellence_salaries',
            'description' => 'Salaries for EXcellence staff outside patient operation costing.',
            'fields' => [
                ['name' => 'salary_number', 'label' => 'Salary Number', 'type' => 'text', 'required' => true],
                ['name' => 'salary_date', 'label' => 'Salary Date', 'type' => 'date', 'required' => true],
                ['name' => 'employee_name', 'label' => 'Employee Name', 'type' => 'text', 'required' => true],
                ['name' => 'role_title', 'label' => 'Role Title', 'type' => 'text'],
                ['name' => 'amount', 'label' => 'Amount', 'type' => 'number', 'required' => true],
                ['name' => 'payment_method', 'label' => 'Payment Method', 'type' => 'select', 'required' => true, 'options' => [
                    'cash' => 'Cash',
                    'bank' => 'Bank',
                    'card' => 'Card',
                    'transfer' => 'Transfer',
                ]],
                ['name' => 'payment_account_name', 'label' => 'Paid From', 'type' => 'select', 'lookup' => 'payment_account_name', 'required' => true],
                ['name' => 'salary_expense_account_code', 'label' => 'Salary Expense Account', 'type' => 'select', 'lookup' => 'expense_account_code', 'required' => true],
                ['name' => 'cost_center_code', 'label' => 'Cost Center', 'type' => 'select', 'lookup' => 'cost_center_code', 'required' => true],
                ['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [
                    'draft' => 'Draft',
                    'posted' => 'Posted',
                    'cancelled' => 'Cancelled',
                ]],
                ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea'],
            ],
            'list_columns' => ['salary_number', 'salary_date', 'employee_name', 'role_title', 'amount', 'payment_account_name', 'salary_expense_account_code', 'status'],
            'search_fields' => ['salary_number', 'employee_name', 'role_title', 'payment_account_name', 'status'],
        ],
        'report_templates' => [
            'title' => 'Report Templates',
            'icon' => '*',
            'table' => 'report_templates',
            'description' => 'Saved report design templates and chart preferences.',
            'fields' => [
                ['name' => 'template_name', 'label' => 'Template Name', 'type' => 'text', 'required' => true],
                ['name' => 'module_scope', 'label' => 'Module Scope', 'type' => 'text'],
                ['name' => 'chart_type', 'label' => 'Chart Type', 'type' => 'select', 'options' => [
                    'bar' => 'Bar',
                    'line' => 'Line',
                    'pie' => 'Pie',
                    'doughnut' => 'Doughnut',
                ]],
                ['name' => 'config_json', 'label' => 'Config JSON', 'type' => 'textarea'],
                ['name' => 'is_active', 'label' => 'Active', 'type' => 'checkbox'],
            ],
            'list_columns' => ['template_name', 'module_scope', 'chart_type', 'is_active', 'created_at'],
            'search_fields' => ['template_name', 'module_scope', 'chart_type'],
        ],
    ];
}

function app_module_definition(string $slug): array
{
    $definitions = app_module_definitions();
    if (!isset($definitions[$slug])) {
        throw new RuntimeException('Unknown module: ' . $slug);
    }

    $definition = $definitions[$slug];
    $definition['slug'] = $slug;
    $definition['admin_only'] = (bool) ($definition['admin_only'] ?? false);
    $definition['scoped'] = (bool) ($definition['scoped'] ?? true);
    $definition['fields'] = is_array($definition['fields'] ?? null) ? $definition['fields'] : [];
    $definition['list_columns'] = is_array($definition['list_columns'] ?? null) ? $definition['list_columns'] : [];
    $definition['search_fields'] = is_array($definition['search_fields'] ?? null) ? $definition['search_fields'] : [];
    $definition = app_localize_module_definition($definition);
    return $definition;
}

function app_module_date_filter_column(array $definition): string
{
    foreach ((array) ($definition['fields'] ?? []) as $field) {
        $name = (string) ($field['name'] ?? '');
        $type = strtolower((string) ($field['type'] ?? 'text'));
        if ($name !== '' && $type === 'date' && app_identifier($name)) {
            return $name;
        }
    }
    return '';
}

function app_visible_modules(): array
{
    $items = [];
    foreach (app_module_definitions() as $slug => $definition) {
        if (!empty($definition['admin_only']) && !app_is_admin()) {
            continue;
        }
        if (!app_current_role_can((string) $slug, 'access')) {
            continue;
        }
        $definition['slug'] = $slug;
        $definition = app_localize_module_definition($definition);
        $items[] = $definition;
    }

    return $items;
}

function app_current_role_can_any_module(array $moduleSlugs): bool
{
    foreach ($moduleSlugs as $moduleSlug) {
        $moduleSlug = app_normalize_whitespace((string) $moduleSlug);
        if ($moduleSlug === '') {
            continue;
        }
        $definition = app_module_definitions()[$moduleSlug] ?? null;
        if (!is_array($definition)) {
            continue;
        }
        if (!empty($definition['admin_only']) && !app_is_admin()) {
            continue;
        }
        if (app_current_role_can($moduleSlug, 'access')) {
            return true;
        }
    }

    return false;
}

function app_module_scope_required(string $slug): bool
{
    $definition = app_module_definition($slug);
    return (bool) ($definition['scoped'] ?? true);
}

function app_module_assert_access(string $slug): array
{
    $definition = app_module_definition($slug);
    if (!empty($definition['admin_only']) && !app_is_admin()) {
        throw new RuntimeException('Access denied for this module.');
    }
    if (!app_current_role_can($slug, 'access')) {
        throw new RuntimeException('Access denied for this module.');
    }
    return $definition;
}

function app_module_field_options(array $field): array
{
    $options = $field['options'] ?? [];
    return is_array($options) ? $options : [];
}

function app_module_select_aliases(string $fieldName): array
{
    if ($fieldName !== 'account_type') {
        return [];
    }

    return [
        'asset' => [
            'asset',
            'assets',
            'current asset',
            'current assets',
            'non current asset',
            'non current assets',
            'non-current asset',
            'non-current assets',
            'fixed asset',
            'fixed assets',
            'أصل',
            'اصل',
            'أصول',
            'اصول',
            'الأصول',
            'الاصول',
            'موجودات',
        ],
        'liability' => [
            'liability',
            'liabilities',
            'current liability',
            'current liabilities',
            'non current liability',
            'non current liabilities',
            'non-current liability',
            'non-current liabilities',
            'payable',
            'payables',
            'خصم',
            'خصوم',
            'التزام',
            'التزامات',
            'المطلوبات',
        ],
        'equity' => [
            'equity',
            'capital',
            'owner equity',
            'owners equity',
            'owner capital',
            'retained earnings',
            'حقوق ملكية',
            'حقوق الملكية',
            'رأس المال',
            'راس المال',
            'ملكية',
        ],
        'revenue' => [
            'revenue',
            'revenues',
            'income',
            'sales',
            'sale',
            'operating revenue',
            'إيراد',
            'ايراد',
            'إيرادات',
            'ايرادات',
            'دخل',
            'مبيعات',
        ],
        'expense' => [
            'expense',
            'expenses',
            'cost',
            'costs',
            'operating expense',
            'مصروف',
            'مصروفات',
            'مصاريف',
            'تكلفة',
            'تكاليف',
        ],
    ];
}

function app_module_normalize_select_value(array $field, string $value): string
{
    $value = app_normalize_whitespace($value);
    if ($value === '') {
        return '';
    }

    $options = app_module_field_options($field);
    if (!$options || array_key_exists($value, $options)) {
        return $value;
    }

    $normalizedValue = app_normalize_header_key($value);
    foreach ($options as $optionValue => $label) {
        $optionValue = (string) $optionValue;
        if (
            strtolower($value) === strtolower($optionValue)
            || strtolower($value) === strtolower((string) $label)
            || $normalizedValue === app_normalize_header_key($optionValue)
            || $normalizedValue === app_normalize_header_key((string) $label)
        ) {
            return $optionValue;
        }
    }

    foreach (app_module_select_aliases((string) ($field['name'] ?? '')) as $optionValue => $aliases) {
        if (!array_key_exists($optionValue, $options)) {
            continue;
        }
        foreach ($aliases as $alias) {
            if ($normalizedValue === app_normalize_header_key((string) $alias)) {
                return (string) $optionValue;
            }
        }
    }

    return $value;
}

function app_module_validate_field(array $field, array $input): mixed
{
    $name = (string) ($field['name'] ?? '');
    $type = strtolower((string) ($field['type'] ?? 'text'));
    $required = !empty($field['required']);
    $rawValue = $input[$name] ?? null;

    $value = null;
    switch ($type) {
        case 'number':
            $value = app_number($rawValue);
            break;
        case 'checkbox':
            $raw = strtolower(app_normalize_whitespace((string) ($rawValue ?? '')));
            $value = in_array($raw, ['1', 'true', 'yes', 'on', 'y'], true) ? 1 : 0;
            break;
        case 'date':
            $value = app_normalize_whitespace((string) ($rawValue ?? ''));
            break;
        case 'select':
            $value = app_module_normalize_select_value($field, (string) ($rawValue ?? ''));
            $options = app_module_field_options($field);
            if ($value !== '' && $options && !array_key_exists($value, $options)) {
                throw new RuntimeException('Invalid value for field: ' . $name);
            }
            break;
        case 'password':
            $value = (string) ($rawValue ?? '');
            break;
        default:
            $value = app_normalize_whitespace((string) ($rawValue ?? ''));
            break;
    }

    if ($required) {
        if ($type === 'number') {
            // Zero is valid; no additional check.
        } elseif ($type === 'checkbox') {
            // Checkbox always has a value.
        } elseif (is_string($value) && $value === '') {
            throw new RuntimeException('Required field is missing: ' . $name);
        }
    }

    return $value;
}

function app_module_auto_number_config(string $slug): ?array
{
    return match ($slug) {
        'contacts' => ['field' => 'contact_code', 'prefix' => 'CNT', 'prefix_by' => 'contact_type', 'prefix_map' => ['customer' => 'CUST', 'vendor' => 'VEND', 'both' => 'CONT']],
        'products' => ['field' => 'sku', 'prefix' => 'PROD'],
        'pos_registers' => ['field' => 'register_code', 'prefix' => 'POS'],
        'pos_sessions' => ['field' => 'session_number', 'prefix' => 'POSS'],
        'pos_orders' => ['field' => 'order_number', 'prefix' => 'POSO'],
        'pos_cash_movements' => ['field' => 'movement_number', 'prefix' => 'POSC'],
        'bank_accounts' => ['field' => 'account_number', 'prefix' => 'BANK'],
        'cost_centers' => ['field' => 'center_code', 'prefix' => 'CC'],
        'projects' => ['field' => 'project_code', 'prefix' => 'PRJ'],
        'sales_invoices' => ['field' => 'invoice_number', 'prefix' => 'SINV'],
        'purchase_bills' => ['field' => 'bill_number', 'prefix' => 'PBIL'],
        'expenses' => ['field' => 'expense_number', 'prefix' => 'EXP'],
        'sales_returns' => ['field' => 'return_number', 'prefix' => 'SRET'],
        'purchase_returns' => ['field' => 'return_number', 'prefix' => 'PRET'],
        'expense_returns' => ['field' => 'return_number', 'prefix' => 'ERET'],
        'prepaid_expenses' => ['field' => 'prepaid_number', 'prefix' => 'PRE'],
        'prepaid_amortization_runs' => ['field' => 'run_number', 'prefix' => 'PAM'],
        'payments' => ['field' => 'payment_number', 'prefix' => 'PAY'],
        'assets' => ['field' => 'asset_code', 'prefix' => 'FA'],
        'hr_departments' => ['field' => 'department_code', 'prefix' => 'HRD'],
        'hr_job_titles' => ['field' => 'job_code', 'prefix' => 'JOB'],
        'hr_shifts' => ['field' => 'shift_code', 'prefix' => 'SH'],
        'hr_leave_types' => ['field' => 'leave_type_code', 'prefix' => 'LVT'],
        'hr_employee_contracts' => ['field' => 'contract_number', 'prefix' => 'CTR'],
        'hr_salary_components' => ['field' => 'component_code', 'prefix' => 'SAL'],
        'employees' => ['field' => 'employee_number', 'prefix' => 'EMP'],
        'hr_attendance' => ['field' => 'attendance_number', 'prefix' => 'ATT'],
        'hr_leave_requests' => ['field' => 'request_number', 'prefix' => 'LV'],
        'hr_payroll_items' => ['field' => 'item_number', 'prefix' => 'PRI'],
        'inventory_movements' => ['field' => 'movement_number', 'prefix' => 'INV-MV'],
        'payroll_runs' => ['field' => 'run_number', 'prefix' => 'PR'],
        'student_stages' => ['field' => 'stage_code', 'prefix' => 'STG'],
        'student_grades' => ['field' => 'grade_code', 'prefix' => 'GRD'],
        'student_classes' => ['field' => 'class_code', 'prefix' => 'CLS'],
        'students' => ['field' => 'student_number', 'prefix' => 'STD'],
        'student_revenue_types' => ['field' => 'revenue_type_code', 'prefix' => 'SREV'],
        'student_fee_plans' => ['field' => 'fee_plan_code', 'prefix' => 'SFEE'],
        'student_invoices' => ['field' => 'student_invoice_number', 'prefix' => 'STINV'],
        'student_receipts' => ['field' => 'receipt_number', 'prefix' => 'STRC'],
        'student_refunds' => ['field' => 'voucher_number', 'prefix' => 'STPV'],
        'excellence_hospitals' => ['field' => 'hospital_code', 'prefix' => 'EXH'],
        'excellence_devices' => ['field' => 'device_code', 'prefix' => 'EXD'],
        'excellence_operations' => ['field' => 'operation_number', 'prefix' => 'EXOP'],
        'excellence_hospital_expenses' => ['field' => 'expense_number', 'prefix' => 'EXHE'],
        'excellence_admin_expenses' => ['field' => 'expense_number', 'prefix' => 'EXAE'],
        'excellence_salaries' => ['field' => 'salary_number', 'prefix' => 'EXSAL'],
        'depreciation_runs' => ['field' => 'run_number', 'prefix' => 'DEP'],
        'ifrs16_leases' => ['field' => 'lease_number', 'prefix' => 'LSE'],
        'journal_entries' => ['field' => 'entry_number', 'prefix' => 'JE'],
        default => null,
    };
}

function app_next_document_number(string $slug, ?int $companyId = null, ?string $prefixOverride = null): ?string
{
    $config = app_module_auto_number_config($slug);
    if ($config === null) {
        return null;
    }

    $definition = app_module_assert_access($slug);
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table for auto-numbering.');
    }

    $field = (string) ($config['field'] ?? '');
    $prefix = app_normalize_whitespace((string) ($prefixOverride ?? ($config['prefix'] ?? '')));
    if ($field === '' || $prefix === '' || !app_identifier($field)) {
        return null;
    }

    $pdo = app_pdo();
    $sql = 'SELECT `' . $field . '` AS doc_no FROM `' . $table . '` WHERE `' . $field . '` LIKE :prefix';
    $params = ['prefix' => $prefix . '-%'];

    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            return null;
        }
        $sql .= ' AND company_id = :company_id';
        $params['company_id'] = $companyId;
    }

    if ($slug === 'chart_of_accounts') {
        $sql .= ' ORDER BY account_code ASC, id ASC LIMIT 500';
    } elseif ($slug === 'journal_entries') {
        $sql .= ' ORDER BY entry_date DESC, id DESC LIMIT 500';
    } else {
        $sql .= ' ORDER BY id DESC LIMIT 500';
    }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $rows = $stmt->fetchAll() ?: [];

    $max = 0;
    $pattern = '/^' . preg_quote($prefix, '/') . '-(\d+)$/i';
    foreach ($rows as $row) {
        $docNo = app_normalize_whitespace((string) ($row['doc_no'] ?? ''));
        if ($docNo === '') {
            continue;
        }
        if (preg_match($pattern, $docNo, $matches)) {
            $number = (int) ($matches[1] ?? 0);
            if ($number > $max) {
                $max = $number;
            }
        }
    }

    return $prefix . '-' . str_pad((string) ($max + 1), 6, '0', STR_PAD_LEFT);
}

function app_apply_module_auto_number(string $slug, array $input, ?int $companyId = null): array
{
    $config = app_module_auto_number_config($slug);
    if ($config === null) {
        return $input;
    }

    $field = (string) ($config['field'] ?? '');
    if ($field === '') {
        return $input;
    }

    $current = app_normalize_whitespace((string) ($input[$field] ?? ''));
    if ($current !== '') {
        return $input;
    }

    $prefixOverride = null;
    $prefixBy = app_normalize_whitespace((string) ($config['prefix_by'] ?? ''));
    $prefixMap = is_array($config['prefix_map'] ?? null) ? $config['prefix_map'] : [];
    if ($prefixBy !== '') {
        $prefixKey = app_normalize_whitespace((string) ($input[$prefixBy] ?? ''));
        if ($prefixKey !== '' && isset($prefixMap[$prefixKey])) {
            $prefixOverride = app_normalize_whitespace((string) $prefixMap[$prefixKey]);
        }
    }

    $next = app_next_document_number($slug, $companyId, $prefixOverride);
    if ($next !== null && $next !== '') {
        $input[$field] = $next;
    }

    return $input;
}

function app_module_prepare_payload(string $slug, array $input, bool $isUpdate, ?int $companyId = null): array
{
    $definition = app_module_assert_access($slug);
    $data = [];
    $autoConfig = app_module_auto_number_config($slug);
    $autoField = is_array($autoConfig) ? app_normalize_whitespace((string) ($autoConfig['field'] ?? '')) : '';

    if (!$isUpdate) {
        $input = app_apply_module_auto_number($slug, $input, $companyId);
    }

    foreach ($definition['fields'] as $field) {
        $name = (string) ($field['name'] ?? '');
        if ($name === '') {
            continue;
        }

        if ($isUpdate && $autoField !== '' && $name === $autoField) {
            $rawAuto = app_normalize_whitespace((string) ($input[$name] ?? ''));
            if ($rawAuto === '') {
                continue;
            }
        }

        $value = app_module_validate_field($field, $input);
        if ($name === 'password_plain') {
            if (!$isUpdate && trim((string) $value) === '') {
                throw new RuntimeException('Password is required for new user.');
            }
            if (trim((string) $value) !== '') {
                $data['password_hash'] = password_hash((string) $value, PASSWORD_DEFAULT);
            }
            continue;
        }

        if ($slug === 'users' && $name === 'company_id') {
            $companyId = app_integer($value);
            $data[$name] = $companyId > 0 ? $companyId : null;
            continue;
        }

        if ($slug === 'companies' && $name === 'logo_path' && (string) $value === '') {
            $value = 'assets/logo.svg';
        }
        if ($slug === 'companies' && $name === 'vat_number') {
            $value = preg_replace('/\D+/', '', (string) $value) ?? '';
            if ($value === '' || strlen((string) $value) !== 15) {
                throw new RuntimeException('Company VAT number must be exactly 15 digits.');
            }
        }

        $data[$name] = $value;
    }

    $recordIdForNormalization = app_integer($input['__record_id'] ?? 0);
    if ($recordIdForNormalization > 0 && in_array($slug, ['ifrs16_leases', 'payments'], true)) {
        $data['__record_id'] = $recordIdForNormalization;
    }

    if ($slug === 'roles') {
        $roleCode = app_current_role_normalize((string) ($data['role_code'] ?? ''));
        if ($roleCode === '') {
            throw new RuntimeException('Role code is required.');
        }
        $data['role_code'] = $roleCode;
    }

    if ($slug === 'role_permissions') {
        $roleCode = app_current_role_normalize((string) ($data['role_code'] ?? ''));
        $moduleSlug = app_normalize_whitespace((string) ($data['module_slug'] ?? ''));
        if (!app_role_exists($roleCode, true)) {
            throw new RuntimeException('Select a valid active role.');
        }
        if (!array_key_exists($moduleSlug, app_module_definitions())) {
            throw new RuntimeException('Select a valid module.');
        }
        $data['role_code'] = $roleCode;
        $data['module_slug'] = $moduleSlug;
        if (empty($data['can_access'])) {
            foreach (['can_create', 'can_edit', 'can_delete', 'can_import', 'can_post'] as $permissionField) {
                $data[$permissionField] = 0;
            }
        }
    }

    if ($slug === 'users') {
        $role = app_current_role_normalize((string) ($data['role'] ?? 'accountant'));
        if (!app_role_exists($role, true)) {
            throw new RuntimeException('Select a valid active role.');
        }
        $data['role'] = $role;
    }

    if ($slug === 'financial_statement_groups') {
        $data['group_code'] = strtoupper(app_normalize_whitespace((string) ($data['group_code'] ?? '')));
        $data['group_name'] = app_normalize_whitespace((string) ($data['group_name'] ?? ''));
        $data['statement_type'] = app_normalize_whitespace((string) ($data['statement_type'] ?? 'balance_sheet'));
        $data['main_section'] = app_normalize_whitespace((string) ($data['main_section'] ?? ''));
        $data['sort_order'] = app_integer($data['sort_order'] ?? 0);
        if ($data['group_code'] === '' || $data['group_name'] === '') {
            throw new RuntimeException('Financial statement group code and name are required.');
        }
        if (!in_array((string) $data['statement_type'], ['balance_sheet', 'income_statement'], true)) {
            throw new RuntimeException('Select a valid statement type.');
        }
        $sections = app_financial_statement_main_sections();
        $mainSection = (string) ($data['main_section'] ?? '');
        if (!isset($sections[$mainSection])) {
            throw new RuntimeException('Select a valid main section.');
        }
        if ((string) ($sections[$mainSection]['statement_type'] ?? '') !== (string) $data['statement_type']) {
            throw new RuntimeException('Main section does not belong to the selected statement type.');
        }
    }

    if ($slug === 'chart_of_accounts') {
        $data['financial_statement_group_code'] = app_normalize_whitespace((string) ($data['financial_statement_group_code'] ?? ''));
    }

    if ($slug === 'contacts' && $companyId !== null && $companyId > 0) {
        $withholdingTaxName = app_normalize_whitespace((string) ($data['withholding_tax_name'] ?? ''));
        if ($withholdingTaxName !== '') {
            $tax = app_tax_rate_row_by_name($companyId, $withholdingTaxName, 'withholding');
            if (!$tax) {
                throw new RuntimeException('Select a valid withholding / commercial profit tax code.');
            }
            $data['withholding_tax_rate_percent'] = max(0, app_number($tax['rate_percent'] ?? 0));
        } else {
            $data['withholding_tax_rate_percent'] = 0;
        }
    }

    if (in_array($slug, ['sales_invoices', 'purchase_bills', 'expenses', 'sales_returns', 'purchase_returns', 'expense_returns'], true)) {
        $data = app_normalize_invoice_bill_payload($slug, $data, $companyId);
    }
    if ($slug === 'student_invoices') {
        $data = app_normalize_student_invoice_payload($data, $companyId);
    }
    if ($slug === 'student_receipts') {
        $data = app_normalize_student_receipt_payload($data, $companyId);
    }
    if ($slug === 'student_refunds') {
        $data = app_normalize_student_refund_payload($data, $companyId);
    }
    if ($slug === 'payments') {
        $data = app_normalize_payment_payload($data, $companyId);
    }
    if ($slug === 'prepaid_expenses') {
        $data = app_normalize_prepaid_expense_payload($data, $companyId);
    }
    if ($slug === 'ifrs16_leases') {
        $data = app_normalize_ifrs16_lease_payload($data, $companyId);
    }
    if (in_array($slug, ['excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'], true)) {
        $data = app_normalize_excellence_payload($slug, $data);
    }
    if ($slug === 'hr_leave_requests') {
        $startDate = app_sanitize_date((string) ($data['start_date'] ?? ''), date('Y-m-d'));
        $endDate = app_sanitize_date((string) ($data['end_date'] ?? ''), $startDate);
        if ($startDate > $endDate) {
            [$startDate, $endDate] = [$endDate, $startDate];
        }
        $data['start_date'] = $startDate;
        $data['end_date'] = $endDate;
        $daysCount = app_number($data['days_count'] ?? 0);
        if ($daysCount <= 0) {
            $daysCount = ((strtotime($endDate) - strtotime($startDate)) / 86400) + 1;
        }
        $data['days_count'] = round(max(0, $daysCount), 2);
    }
    if ($slug === 'hr_attendance') {
        $data['attendance_date'] = app_sanitize_date((string) ($data['attendance_date'] ?? ''), date('Y-m-d'));
        $hoursWorked = app_number($data['hours_worked'] ?? 0);
        $checkIn = app_normalize_whitespace((string) ($data['check_in'] ?? ''));
        $checkOut = app_normalize_whitespace((string) ($data['check_out'] ?? ''));
        if ($hoursWorked <= 0 && $checkIn !== '' && $checkOut !== '') {
            $inTime = strtotime($data['attendance_date'] . ' ' . $checkIn);
            $outTime = strtotime($data['attendance_date'] . ' ' . $checkOut);
            if ($inTime !== false && $outTime !== false && $outTime > $inTime) {
                $hoursWorked = ($outTime - $inTime) / 3600;
            }
        }
        $data['hours_worked'] = round(max(0, $hoursWorked), 2);
    }
    if ($slug === 'hr_employee_contracts') {
        $startDate = app_sanitize_date((string) ($data['start_date'] ?? ''), date('Y-m-d'));
        $endDateRaw = app_normalize_whitespace((string) ($data['end_date'] ?? ''));
        $endDate = $endDateRaw !== '' ? app_sanitize_date($endDateRaw, $startDate) : null;
        if ($endDate !== null && $endDate < $startDate) {
            $endDate = $startDate;
        }
        $data['start_date'] = $startDate;
        $data['end_date'] = $endDate;
    }

    return $data;
}

function app_module_list(string $slug, ?int $companyId = null, string $q = '', string $fromDate = '', string $toDate = '', array $filters = []): array
{
    $definition = app_module_assert_access($slug);
    $pdo = app_pdo();
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table.');
    }

    $sql = 'SELECT * FROM `' . $table . '` WHERE 1=1';
    $params = [];
    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            return [];
        }
        $sql .= ' AND company_id = :company_id';
        $params['company_id'] = $companyId;
    }

    $q = app_normalize_whitespace($q);
    if ($q !== '' && !empty($definition['search_fields'])) {
        $parts = [];
        foreach ($definition['search_fields'] as $index => $fieldName) {
            if (!app_identifier((string) $fieldName)) {
                continue;
            }
            $key = 'q_' . $index;
            $parts[] = '`' . $fieldName . '` LIKE :' . $key;
            $params[$key] = '%' . $q . '%';
        }
        if ($parts) {
            $sql .= ' AND (' . implode(' OR ', $parts) . ')';
        }
    }

    $dateColumn = app_module_date_filter_column($definition);
    $fromDate = app_normalize_whitespace($fromDate);
    $toDate = app_normalize_whitespace($toDate);
    if ($dateColumn !== '' && $fromDate !== '') {
        $fromTimestamp = strtotime($fromDate);
        if ($fromTimestamp !== false) {
            $fromDate = date('Y-m-d', $fromTimestamp);
            $sql .= ' AND `' . $dateColumn . '` >= :from_date';
            $params['from_date'] = $fromDate;
        }
    }
    if ($dateColumn !== '' && $toDate !== '') {
        $toTimestamp = strtotime($toDate);
        if ($toTimestamp !== false) {
            $toDate = date('Y-m-d', $toTimestamp);
            $sql .= ' AND `' . $dateColumn . '` <= :to_date';
            $params['to_date'] = $toDate;
        }
    }

    $contactFilter = app_normalize_whitespace((string) ($filters['contact_code'] ?? ''));
    if ($contactFilter !== '' && in_array($slug, ['sales_invoices', 'student_invoices', 'purchase_bills'], true)) {
        $contactColumn = match ($slug) {
            'sales_invoices' => 'customer_code',
            'student_invoices' => 'guardian_contact_code',
            default => 'vendor_code',
        };
        $sql .= ' AND `' . $contactColumn . '` = :contact_filter';
        $params['contact_filter'] = $contactFilter;
    }

    if ($slug === 'chart_of_accounts') {
        $sql .= ' ORDER BY account_code ASC, id ASC LIMIT 500';
    } elseif ($slug === 'journal_entries') {
        $sql .= ' ORDER BY entry_date DESC, id DESC LIMIT 500';
    } else {
        $sql .= ' ORDER BY id DESC LIMIT 500';
    }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $rows = $stmt->fetchAll() ?: [];
    if (!$rows) {
        return [];
    }

    if ($slug === 'users') {
        foreach ($rows as &$row) {
            $row['company_access'] = app_user_company_names((int) ($row['id'] ?? 0));
        }
        unset($row);
    }

    if ($slug === 'journal_entries' && $companyId !== null && $companyId > 0) {
        $entryIds = [];
        foreach ($rows as $row) {
            $entryId = (int) ($row['id'] ?? 0);
            if ($entryId > 0) {
                $entryIds[$entryId] = true;
            }
        }

        $lineSummaries = [];
        if ($entryIds) {
            $placeholders = [];
            $lineParams = ['company_id' => $companyId];
            $entryIdList = array_values(array_keys($entryIds));
            foreach ($entryIdList as $index => $entryId) {
                $key = 'entry_' . $index;
                $placeholders[] = ':' . $key;
                $lineParams[$key] = $entryId;
            }

            $lineSql = 'SELECT
                            journal_entry_id,
                            COUNT(*) AS line_items_count,
                            COALESCE(SUM(debit), 0) AS line_items_total_debit,
                            COALESCE(SUM(credit), 0) AS line_items_total_credit
                        FROM journal_entry_lines
                        WHERE company_id = :company_id
                          AND journal_entry_id IN (' . implode(', ', $placeholders) . ')
                        GROUP BY journal_entry_id';
            $lineStmt = $pdo->prepare($lineSql);
            $lineStmt->execute($lineParams);
            foreach (($lineStmt->fetchAll() ?: []) as $lineSummaryRow) {
                $summaryEntryId = (int) ($lineSummaryRow['journal_entry_id'] ?? 0);
                if ($summaryEntryId <= 0) {
                    continue;
                }
                $lineSummaries[$summaryEntryId] = [
                    'line_items_count' => (int) ($lineSummaryRow['line_items_count'] ?? 0),
                    'line_items_total_debit' => app_number($lineSummaryRow['line_items_total_debit'] ?? 0),
                    'line_items_total_credit' => app_number($lineSummaryRow['line_items_total_credit'] ?? 0),
                ];
            }
        }

        foreach ($rows as &$row) {
            $entryId = (int) ($row['id'] ?? 0);
            $summary = $lineSummaries[$entryId] ?? [
                'line_items_count' => 0,
                'line_items_total_debit' => 0.0,
                'line_items_total_credit' => 0.0,
            ];
            $row['line_items_count'] = (int) $summary['line_items_count'];
            $row['line_items_total_debit'] = (float) $summary['line_items_total_debit'];
            $row['line_items_total_credit'] = (float) $summary['line_items_total_credit'];
        }
        unset($row);
    }

    if ($companyId !== null && $companyId > 0 && app_module_requires_auto_posting($slug)) {
        $sourceIds = [];
        foreach ($rows as $row) {
            $sourceId = (int) ($row['id'] ?? 0);
            if ($sourceId > 0) {
                $sourceIds[] = $sourceId;
            }
        }
        $journalIndex = app_journal_entries_index_for_source_ids($companyId, $slug, $sourceIds);
        foreach ($rows as &$row) {
            $sourceId = (int) ($row['id'] ?? 0);
            $linked = $journalIndex[$sourceId] ?? null;
            if (is_array($linked)) {
                $row['__journal_entry_id'] = (int) ($linked['id'] ?? 0);
                $row['__journal_entry_number'] = (string) ($linked['entry_number'] ?? '');
            } else {
                $row['__journal_entry_id'] = 0;
                $row['__journal_entry_number'] = '';
            }
        }
        unset($row);
    }

    if (in_array($slug, ['purchase_bills', 'expenses'], true) && $companyId !== null && $companyId > 0) {
        foreach ($rows as &$row) {
            $summary = app_budget_summary_for_expense_account(
                $companyId,
                (string) ($row['expense_account_code'] ?? ''),
                (string) ($row[$slug === 'expenses' ? 'expense_date' : 'bill_date'] ?? date('Y-m-d'))
            );
            $row['budget_amount'] = (float) ($summary['budget_amount'] ?? 0);
            $row['budget_spent'] = (float) ($summary['spent_amount'] ?? 0);
            $row['budget_remaining'] = (float) ($summary['remaining_amount'] ?? 0);
            $row['budget_usage_percent'] = (float) ($summary['usage_percent'] ?? 0);
            $row['budget_fiscal_year'] = (string) ($summary['fiscal_year'] ?? '');
        }
        unset($row);
    }

    return $rows;
}

function app_module_find(string $slug, int $id, ?int $companyId = null): ?array
{
    if ($id <= 0) {
        return null;
    }

    $definition = app_module_assert_access($slug);
    $pdo = app_pdo();
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table.');
    }

    $sql = 'SELECT * FROM `' . $table . '` WHERE id = :id';
    $params = ['id' => $id];
    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            return null;
        }
        $sql .= ' AND company_id = :company_id';
        $params['company_id'] = $companyId;
    }
    $sql .= ' LIMIT 1';

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $row = $stmt->fetch();
    return $row ?: null;
}

function app_module_save(string $slug, array $input, ?int $companyId = null, ?int $id = null): int
{
    $definition = app_module_assert_access($slug);
    $pdo = app_pdo();
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table.');
    }

    $isUpdate = $id !== null && $id > 0;
    app_enforce_current_role_can($slug, $isUpdate ? 'edit' : 'create');
    if ($isUpdate) {
        $input['__record_id'] = (int) $id;
    }
    $data = app_module_prepare_payload($slug, $input, $isUpdate, $companyId);
    $documentLines = null;

    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            throw new RuntimeException('Company context is required for this module.');
        }
        $data['company_id'] = $companyId;
    }

    if (in_array($slug, app_document_line_slugs(), true) && $companyId !== null && $companyId > 0) {
        $documentLines = app_parse_document_lines($slug, $input, $data, $companyId);
        $data = app_apply_document_line_totals($slug, $data, $documentLines);
        app_validate_document_lines($slug, $companyId, $documentLines);
    }

    app_validate_transaction_master_links($slug, $data, $companyId);
    foreach (array_keys($data) as $dataKey) {
        if (str_starts_with((string) $dataKey, '__')) {
            unset($data[$dataKey]);
        }
    }

    if (!$isUpdate && $slug === 'posting_account_settings' && $companyId !== null && $companyId > 0) {
        $existingSettingsStmt = $pdo->prepare(
            'SELECT id
             FROM posting_account_settings
             WHERE company_id = :company_id
             LIMIT 1'
        );
        $existingSettingsStmt->execute(['company_id' => $companyId]);
        $existingSettingsId = (int) ($existingSettingsStmt->fetchColumn() ?: 0);
        if ($existingSettingsId > 0) {
            $id = $existingSettingsId;
            $isUpdate = true;
        }
    }

    if (!$isUpdate && $slug === 'role_permissions') {
        $existingPermissionStmt = $pdo->prepare(
            'SELECT id
             FROM role_permissions
             WHERE role_code = :role_code
               AND module_slug = :module_slug
             LIMIT 1'
        );
        $existingPermissionStmt->execute([
            'role_code' => (string) ($data['role_code'] ?? ''),
            'module_slug' => (string) ($data['module_slug'] ?? ''),
        ]);
        $existingPermissionId = (int) ($existingPermissionStmt->fetchColumn() ?: 0);
        if ($existingPermissionId > 0) {
            $id = $existingPermissionId;
            $isUpdate = true;
        }
    }

    $startedTransaction = false;
    $existingBeforeSave = null;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        if (!$isUpdate) {
            $columns = array_keys($data);
            if (!$columns) {
                throw new RuntimeException('No data to insert.');
            }

            $placeholders = array_map(static fn (string $column): string => ':' . $column, $columns);
            $sql = 'INSERT INTO `' . $table . '` (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $placeholders) . ')';
            $stmt = $pdo->prepare($sql);
            $stmt->execute($data);
            $id = (int) $pdo->lastInsertId();
        } else {
            $id = (int) $id;
            $existing = app_module_find($slug, $id, $companyId);
            if (!$existing) {
                throw new RuntimeException('Record was not found.');
            }
            $existingBeforeSave = $existing;

            if ($slug === 'roles') {
                $oldRoleCode = app_current_role_normalize((string) ($existing['role_code'] ?? ''));
                $newRoleCode = app_current_role_normalize((string) ($data['role_code'] ?? $oldRoleCode));
                if ($newRoleCode !== $oldRoleCode) {
                    throw new RuntimeException('Role code cannot be changed after creation.');
                }
                $data['role_code'] = $oldRoleCode;
            }

            if ($slug === 'users' && !array_key_exists('password_hash', $data)) {
                // Keep old hash when password is empty.
            }

            if ($data) {
                $setParts = [];
                foreach (array_keys($data) as $column) {
                    $setParts[] = $column . ' = :' . $column;
                }

                $sql = 'UPDATE `' . $table . '` SET ' . implode(', ', $setParts) . ' WHERE id = :id';
                if (!empty($definition['scoped'])) {
                    $sql .= ' AND company_id = :scope_company_id';
                    $data['scope_company_id'] = $companyId;
                }
                $data['id'] = $id;

                $stmt = $pdo->prepare($sql);
                $stmt->execute($data);
            }
        }

        if ($documentLines !== null && $id > 0 && $companyId !== null && $companyId > 0) {
            app_replace_document_lines($slug, $companyId, (int) $id, $documentLines);
        }

        if (app_module_requires_auto_posting($slug) && $id > 0 && $companyId !== null && $companyId > 0) {
            $savedRecord = app_module_find($slug, (int) $id, $companyId);
            if ($savedRecord) {
                app_sync_auto_posting_for_record($slug, $savedRecord, $companyId);
            }
        }
        if ($slug === 'payments' && $isUpdate && $existingBeforeSave && $companyId !== null && $companyId > 0) {
            app_sync_payment_related_documents($companyId, $existingBeforeSave);
        }
        if ($slug === 'prepaid_amortization_runs' && $id > 0 && $companyId !== null && $companyId > 0) {
            $savedRun = app_module_find($slug, (int) $id, $companyId);
            if ($savedRun) {
                app_refresh_prepaid_amortized_amount($companyId, (int) ($savedRun['prepaid_expense_id'] ?? 0));
            }
            if ($existingBeforeSave) {
                app_refresh_prepaid_amortized_amount($companyId, (int) ($existingBeforeSave['prepaid_expense_id'] ?? 0));
            }
        }
        if ($slug === 'themes' && $id > 0 && !empty($data['is_default'])) {
            $pdo->prepare('UPDATE themes SET is_default = 0 WHERE id <> :id')
                ->execute(['id' => (int) $id]);
            $pdo->prepare('UPDATE themes SET is_default = 1 WHERE id = :id')
                ->execute(['id' => (int) $id]);
            $themeStmt = $pdo->prepare('SELECT * FROM themes WHERE id = :id LIMIT 1');
            $themeStmt->execute(['id' => (int) $id]);
            $themeRecord = $themeStmt->fetch();
            if (is_array($themeRecord)) {
                app_apply_ui_theme_record($themeRecord);
            }
        }
        if ($slug === 'themes' && $id > 0) {
            $activeDefaultCount = (int) ($pdo->query('SELECT COUNT(*) FROM themes WHERE is_default = 1 AND is_active = 1')->fetchColumn() ?: 0);
            if ($activeDefaultCount === 0) {
                $pdo->prepare('UPDATE themes SET is_default = 1, is_active = 1 WHERE id = :id')
                    ->execute(['id' => (int) $id]);
                $themeStmt = $pdo->prepare('SELECT * FROM themes WHERE id = :id LIMIT 1');
                $themeStmt->execute(['id' => (int) $id]);
                $themeRecord = $themeStmt->fetch();
                if (is_array($themeRecord)) {
                    app_apply_ui_theme_record($themeRecord);
                }
            }
        }

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }
        return (int) $id;
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_module_delete(string $slug, int $id, ?int $companyId = null): void
{
    if ($id <= 0) {
        throw new RuntimeException('Invalid record identifier.');
    }

    $definition = app_module_assert_access($slug);
    $pdo = app_pdo();
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table.');
    }
    app_enforce_current_role_can($slug, 'delete');

    $record = app_module_find($slug, $id, $companyId);
    if (!$record) {
        throw new RuntimeException('Record was not found.');
    }

    if ($slug === 'roles') {
        $roleCode = app_current_role_normalize((string) ($record['role_code'] ?? ''));
        if (!empty($record['is_system'])) {
            throw new RuntimeException('System roles cannot be deleted.');
        }
        $assignedStmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE role = :role_code');
        $assignedStmt->execute(['role_code' => $roleCode]);
        if ((int) ($assignedStmt->fetchColumn() ?: 0) > 0) {
            throw new RuntimeException('This role is assigned to users and cannot be deleted.');
        }
    }

    $ifrs16SchedulesForDelete = [];
    if ($slug === 'ifrs16_lease_schedules' && $companyId !== null && $companyId > 0) {
        $scheduleStatus = strtolower(app_normalize_whitespace((string) ($record['status'] ?? 'draft')));
        $journal = app_journal_entry_for_source((int) $companyId, 'ifrs16_lease_schedules', $id);
        $journalStatus = strtolower(app_normalize_whitespace((string) ($journal['status'] ?? '')));
        if ($scheduleStatus === 'posted' || $journalStatus === 'posted') {
            throw new RuntimeException('Reset the IFRS16 schedule journal to draft before deleting it.');
        }
    }

    if ($slug === 'ifrs16_leases' && $companyId !== null && $companyId > 0) {
        $initialJournal = app_journal_entry_for_source((int) $companyId, 'ifrs16_leases', $id);
        if ($initialJournal && strtolower(app_normalize_whitespace((string) ($initialJournal['status'] ?? ''))) === 'posted') {
            throw new RuntimeException('Reset the initial IFRS16 journal to draft before deleting the lease.');
        }
        $ifrs16SchedulesForDelete = app_ifrs16_lease_schedule_lines((int) $companyId, $id);
        foreach ($ifrs16SchedulesForDelete as $scheduleForDelete) {
            $scheduleStatus = strtolower(app_normalize_whitespace((string) ($scheduleForDelete['status'] ?? 'draft')));
            $journalStatus = strtolower(app_normalize_whitespace((string) ($scheduleForDelete['journal_status'] ?? '')));
            if ($scheduleStatus === 'posted' || $journalStatus === 'posted') {
                throw new RuntimeException('Reset all IFRS16 schedule journals to draft before deleting the lease.');
            }
        }
    }

    $startedTransaction = false;
    try {
        if (!$pdo->inTransaction()) {
            $pdo->beginTransaction();
            $startedTransaction = true;
        }

        if (in_array($slug, app_document_line_slugs(), true) && $companyId !== null && $companyId > 0) {
            app_replace_document_lines($slug, $companyId, $id, []);
        }

        if ($slug === 'journal_entries') {
            $pdo->prepare(
                'DELETE FROM journal_entry_lines
                 WHERE company_id = :company_id
                   AND journal_entry_id = :journal_entry_id'
            )->execute([
                'company_id' => $companyId,
                'journal_entry_id' => $id,
            ]);
        }

        if ($slug === 'prepaid_expenses' && $companyId !== null && $companyId > 0) {
            $runStmt = $pdo->prepare(
                'SELECT id
                 FROM prepaid_amortization_runs
                 WHERE company_id = :company_id
                   AND prepaid_expense_id = :prepaid_expense_id'
            );
            $runStmt->execute([
                'company_id' => $companyId,
                'prepaid_expense_id' => $id,
            ]);
            $runIds = array_map('intval', $runStmt->fetchAll(PDO::FETCH_COLUMN) ?: []);
            foreach ($runIds as $runId) {
                app_delete_auto_journal_entry($companyId, 'prepaid_amortization_runs', $runId);
            }
            $pdo->prepare(
                'DELETE FROM prepaid_amortization_runs
                 WHERE company_id = :company_id
                   AND prepaid_expense_id = :prepaid_expense_id'
            )->execute([
                'company_id' => $companyId,
                'prepaid_expense_id' => $id,
            ]);
        }

        if ($slug === 'ifrs16_leases' && $companyId !== null && $companyId > 0) {
            foreach ($ifrs16SchedulesForDelete as $scheduleForDelete) {
                app_delete_auto_journal_entry((int) $companyId, 'ifrs16_lease_schedules', (int) ($scheduleForDelete['id'] ?? 0));
            }
            $pdo->prepare(
                'DELETE FROM ifrs16_lease_schedules
                 WHERE company_id = :company_id
                   AND lease_id = :lease_id'
            )->execute([
                'company_id' => (int) $companyId,
                'lease_id' => $id,
            ]);
            app_delete_auto_journal_entry((int) $companyId, 'ifrs16_leases', $id);
        }

        if ($slug === 'roles') {
            $pdo->prepare('DELETE FROM role_permissions WHERE role_code = :role_code')
                ->execute(['role_code' => app_current_role_normalize((string) ($record['role_code'] ?? ''))]);
        }

        if (in_array($slug, ['pos_orders', 'pos_sessions', 'pos_cash_movements'], true)) {
            throw new RuntimeException('POS records are an accounting audit trail. Use refunds, cash movements, or session closing instead of deleting them.');
        }

        $sql = 'DELETE FROM `' . $table . '` WHERE id = :id';
        $params = ['id' => $id];
        if (!empty($definition['scoped'])) {
            if ($companyId === null || $companyId <= 0) {
                throw new RuntimeException('Company context is required.');
            }
            $sql .= ' AND company_id = :company_id';
            $params['company_id'] = $companyId;
        }

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);

        if (app_module_requires_auto_posting($slug) && $companyId !== null && $companyId > 0) {
            app_delete_auto_journal_entry($companyId, $slug, $id);
            if ($slug === 'payments') {
                app_sync_payment_related_documents($companyId, $record);
            } elseif ($slug === 'sales_invoices') {
                app_sync_document_payment_status_by_reference($companyId, (string) ($record['invoice_number'] ?? ''));
            } elseif ($slug === 'student_invoices') {
                app_sync_student_invoice_payment_status($companyId, (int) ($record['id'] ?? 0));
            } elseif ($slug === 'student_receipts') {
                app_sync_student_receipt_related_invoice($companyId, $record);
            } elseif ($slug === 'purchase_bills') {
                app_sync_document_payment_status_by_reference($companyId, (string) ($record['bill_number'] ?? ''));
            } elseif ($slug === 'expenses') {
                app_sync_document_payment_status_by_reference($companyId, (string) ($record['expense_number'] ?? ''));
            } elseif ($slug === 'prepaid_amortization_runs') {
                app_refresh_prepaid_amortized_amount($companyId, (int) ($record['prepaid_expense_id'] ?? 0));
            }
        }

        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->commit();
        }
    } catch (Throwable $e) {
        if ($startedTransaction && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}

function app_module_counts(?int $companyId = null): array
{
    $pdo = app_pdo();
    $counts = [];

    foreach (app_visible_modules() as $module) {
        $slug = (string) ($module['slug'] ?? '');
        $table = (string) ($module['table'] ?? '');
        if ($slug === '' || !app_identifier($table)) {
            continue;
        }

        $sql = 'SELECT COUNT(*) FROM `' . $table . '`';
        $params = [];
        if (!empty($module['scoped'])) {
            if ($companyId === null || $companyId <= 0) {
                $counts[$slug] = 0;
                continue;
            }
            $sql .= ' WHERE company_id = :company_id';
            $params['company_id'] = $companyId;
        }

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $counts[$slug] = (int) ($stmt->fetchColumn() ?: 0);
    }

    return $counts;
}

function app_module_value_label(string $slug, array $field, mixed $value): string
{
    $type = strtolower((string) ($field['type'] ?? 'text'));
    if ($type === 'checkbox') {
        return (int) $value === 1 ? app_t('Yes') : app_t('No');
    }
    if ($type === 'number') {
        return app_money_format($value);
    }
    if ($type === 'select') {
        $options = app_module_field_options($field);
        $key = (string) $value;
        return isset($options[$key]) ? app_t((string) $options[$key]) : app_t($key);
    }
    return app_t((string) $value);
}

function app_logo_path_for_header(?array $company): string
{
    return app_company_logo_path($company);
}

function app_zatca_tlv_base64(?array $company, array $invoice, string $dateField = 'invoice_date'): string
{
    $sellerName = app_normalize_whitespace((string) ($company['legal_name'] ?? ''));
    if ($sellerName === '') {
        $sellerName = app_normalize_whitespace((string) ($company['name'] ?? app_config('app_name')));
    }
    $vatNumber = preg_replace('/\D+/', '', app_normalize_whitespace((string) ($company['vat_number'] ?? ''))) ?? '';
    $dateValue = app_sanitize_date((string) ($invoice[$dateField] ?? ''), date('Y-m-d'));
    $timestamp = gmdate('Y-m-d\T00:00:00\Z', strtotime($dateValue . ' 00:00:00') ?: time());
    $total = number_format(max(0, app_number($invoice['grand_total'] ?? 0)), 2, '.', '');
    $vatTotal = number_format(max(0, app_number($invoice['tax_total'] ?? 0)), 2, '.', '');

    $fields = [
        1 => $sellerName,
        2 => $vatNumber,
        3 => $timestamp,
        4 => $total,
        5 => $vatTotal,
    ];

    $payload = '';
    foreach ($fields as $tag => $value) {
        $bytes = (string) $value;
        if (function_exists('mb_strcut')) {
            $bytes = mb_strcut($bytes, 0, 255, 'UTF-8');
        } else {
            $bytes = substr($bytes, 0, 255);
        }
        $payload .= chr((int) $tag) . chr(strlen($bytes)) . $bytes;
    }

    return base64_encode($payload);
}

function app_amount_in_words_ar(float $amount, string $major = 'ريال سعودي', string $minor = 'هللة'): string
{
    $amount = round(max(0, $amount), 2);
    $whole = (int) floor($amount);
    $fraction = (int) round(($amount - $whole) * 100);

    $ones = [
        0 => 'صفر', 1 => 'واحد', 2 => 'اثنان', 3 => 'ثلاثة', 4 => 'أربعة',
        5 => 'خمسة', 6 => 'ستة', 7 => 'سبعة', 8 => 'ثمانية', 9 => 'تسعة',
        10 => 'عشرة', 11 => 'أحد عشر', 12 => 'اثنا عشر', 13 => 'ثلاثة عشر',
        14 => 'أربعة عشر', 15 => 'خمسة عشر', 16 => 'ستة عشر', 17 => 'سبعة عشر',
        18 => 'ثمانية عشر', 19 => 'تسعة عشر',
    ];
    $tens = [20 => 'عشرون', 30 => 'ثلاثون', 40 => 'أربعون', 50 => 'خمسون', 60 => 'ستون', 70 => 'سبعون', 80 => 'ثمانون', 90 => 'تسعون'];
    $hundreds = [1 => 'مائة', 2 => 'مائتان', 3 => 'ثلاثمائة', 4 => 'أربعمائة', 5 => 'خمسمائة', 6 => 'ستمائة', 7 => 'سبعمائة', 8 => 'ثمانمائة', 9 => 'تسعمائة'];

    $belowThousand = static function (int $number) use (&$belowThousand, $ones, $tens, $hundreds): string {
        $number = max(0, min(999, $number));
        if ($number < 20) {
            return $ones[$number];
        }
        if ($number < 100) {
            $unit = $number % 10;
            $ten = $number - $unit;
            return $unit > 0 ? ($ones[$unit] . ' و' . $tens[$ten]) : $tens[$ten];
        }
        $hundred = intdiv($number, 100);
        $rest = $number % 100;
        return $rest > 0 ? ($hundreds[$hundred] . ' و' . $belowThousand($rest)) : $hundreds[$hundred];
    };

    $groupText = static function (int $value, string $singular, string $dual, string $plural) use ($belowThousand): string {
        if ($value <= 0) {
            return '';
        }
        if ($value === 1) {
            return $singular;
        }
        if ($value === 2) {
            return $dual;
        }
        if ($value <= 10) {
            return $belowThousand($value) . ' ' . $plural;
        }
        return $belowThousand($value) . ' ' . $singular;
    };

    $numberToWords = static function (int $number) use ($belowThousand, $groupText): string {
        if ($number <= 0) {
            return 'صفر';
        }
        $parts = [];
        $billions = intdiv($number, 1000000000);
        $number %= 1000000000;
        $millions = intdiv($number, 1000000);
        $number %= 1000000;
        $thousands = intdiv($number, 1000);
        $number %= 1000;

        foreach ([[$billions, 'مليار', 'ملياران', 'مليارات'], [$millions, 'مليون', 'مليونان', 'ملايين'], [$thousands, 'ألف', 'ألفان', 'آلاف']] as $group) {
            $text = $groupText((int) $group[0], (string) $group[1], (string) $group[2], (string) $group[3]);
            if ($text !== '') {
                $parts[] = $text;
            }
        }
        if ($number > 0) {
            $parts[] = $belowThousand($number);
        }
        return implode(' و', $parts);
    };

    $words = $numberToWords($whole) . ' ' . $major;
    if ($fraction > 0) {
        $words .= ' و' . $numberToWords($fraction) . ' ' . $minor;
    }
    return 'فقط ' . $words . ' لا غير';
}

function app_back_button_html(string $label = 'Back'): string
{
    return '<a class="floating-back-btn" href="#" role="button" onclick="history.back(); return false;">' . app_escape(app_t($label)) . '</a>';
}

function app_accounting_workspace_module_slugs(): array
{
    return [
        'journal_entries',
        'sales_invoices',
        'purchase_bills',
        'expenses',
        'sales_returns',
        'purchase_returns',
        'expense_returns',
        'prepaid_expenses',
        'prepaid_amortization_runs',
        'payments',
        'inventory_movements',
        'assets',
        'depreciation_runs',
        'ifrs16_leases',
        'ifrs16_lease_schedules',
        'companies',
        'users',
        'roles',
        'role_permissions',
        'themes',
        'chart_of_accounts',
        'financial_statement_groups',
        'ecl_report_categories',
        'ecl_countries',
        'ecl_default_rates',
        'ecl_receivable_buckets',
        'posting_account_settings',
        'contacts',
        'products',
        'tax_rates',
        'bank_accounts',
        'cash_accounts',
        'cost_centers',
        'projects',
        'budgets',
    ];
}

function app_pos_workspace_module_slugs(): array
{
    return [
        'pos_sessions',
        'pos_orders',
        'pos_cash_movements',
        'pos_registers',
    ];
}

function app_hr_workspace_module_slugs(): array
{
    return [
        'hr_departments',
        'hr_job_titles',
        'hr_shifts',
        'hr_leave_types',
        'hr_employee_contracts',
        'hr_salary_components',
        'employees',
        'hr_attendance',
        'hr_leave_requests',
        'payroll_runs',
        'hr_payroll_items',
    ];
}

function app_student_workspace_module_slugs(): array
{
    return [
        'student_invoices',
        'student_receipts',
        'student_refunds',
        'students',
        'student_stages',
        'student_grades',
        'student_classes',
        'student_revenue_types',
        'student_fee_plans',
        'student_account_settings',
    ];
}

function app_excellence_workspace_module_slugs(): array
{
    return [
        'excellence_operations',
        'excellence_hospital_expenses',
        'excellence_admin_expenses',
        'excellence_salaries',
        'excellence_hospitals',
        'excellence_devices',
    ];
}

function app_workspace_for_module(?string $activeModule): string
{
    $activeModule = app_normalize_whitespace((string) ($activeModule ?? ''));
    if ($activeModule !== '' && in_array($activeModule, app_hr_workspace_module_slugs(), true)) {
        return 'hr';
    }
    if ($activeModule !== '' && in_array($activeModule, app_student_workspace_module_slugs(), true)) {
        return 'students';
    }
    if ($activeModule !== '' && in_array($activeModule, app_excellence_workspace_module_slugs(), true)) {
        return 'excellence';
    }
    if ($activeModule !== '' && in_array($activeModule, app_pos_workspace_module_slugs(), true)) {
        return 'pos';
    }
    return 'accounting';
}

function app_sidebar_nav_html(string $activePage = 'dashboard', ?string $activeModule = null, ?int $companyId = null, ?string $activeReport = null): string
{
    $user = app_current_user() ?? [];
    $displayName = app_normalize_whitespace((string) ($user['full_name'] ?? ''));
    if ($displayName === '') {
        $displayName = app_current_username();
    }

    $companyName = app_current_company_name();
    if (app_is_admin()) {
        if ($companyId !== null && $companyId > 0) {
            $company = app_company_by_id($companyId);
            if ($company) {
                $companyName = (string) ($company['name'] ?? $companyName);
            }
        }
        if ($companyName === null || $companyName === '') {
            $companyName = app_t('All Companies');
        }
    } elseif ($companyName === null || $companyName === '') {
        $companyName = app_t('No Company');
    }

    $companyQuery = '';
    if ($companyId !== null && $companyId > 0) {
        $companyQuery = '&company_id=' . urlencode((string) $companyId);
    }

    $renderLink = static function (array $link, string $baseClass = 'site-sidebar__link'): string {
        $classes = $baseClass;
        if (!empty($link['active'])) {
            $classes .= ' is-active';
        }
        if (!empty($link['logout'])) {
            $classes .= ' site-sidebar__link--logout';
        }

        $html = '<a class="' . app_escape($classes) . '" href="' . app_escape((string) $link['href']) . '"' . (!empty($link['active']) ? ' aria-current="page"' : '') . '>';
        $html .= '<span class="site-sidebar__icon">' . app_escape((string) $link['icon']) . '</span>';
        $html .= '<span class="site-sidebar__label">' . app_escape((string) $link['label']) . '</span>';
        $html .= '</a>';
        return $html;
    };

    $homeHref = 'home.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
    $dashboardHref = 'index.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
    $quickLinksHtml = '';
    $quickLinksHtml .= $renderLink([
        'label' => app_t('Home'),
        'href' => $homeHref,
        'icon' => 'HM',
        'active' => $activePage === 'home',
    ], 'site-sidebar__link top-mega-nav__quick-link');
    $quickLinksHtml .= $renderLink([
        'label' => app_t('Dashboard'),
        'href' => $dashboardHref,
        'icon' => 'DB',
        'active' => $activePage === 'dashboard',
    ], 'site-sidebar__link top-mega-nav__quick-link');

    $reportCatalog = app_accounting_reports_catalog();
    $accountingActive = $activePage === 'accounting_reports';
    $defaultReport = array_key_first($reportCatalog);
    $normalizedActiveReport = app_normalize_whitespace((string) ($activeReport ?? ''));
    if ($normalizedActiveReport === '' || !array_key_exists($normalizedActiveReport, $reportCatalog)) {
        $normalizedActiveReport = is_string($defaultReport) ? $defaultReport : 'ledger';
    }
    $subLinksHtml = '';
    foreach ($reportCatalog as $reportKey => $item) {
        $href = 'accounting_reports.php?report=' . urlencode((string) $reportKey);
        if ($companyId !== null && $companyId > 0) {
            $href .= '&company_id=' . urlencode((string) $companyId);
        }
        $subClass = 'site-sidebar__sublink';
        if ($accountingActive && $normalizedActiveReport === (string) $reportKey) {
            $subClass .= ' is-active';
        }
        $subLinksHtml .= '<a class="' . app_escape($subClass) . '" href="' . app_escape($href) . '">';
        $subLinksHtml .= '<span class="site-sidebar__subicon">' . app_escape((string) ($item['icon'] ?? 'AR')) . '</span>';
        $subLinksHtml .= '<span class="site-sidebar__sublabel">' . app_escape((string) ($item['label'] ?? $reportKey)) . '</span>';
        $subLinksHtml .= '</a>';
    }

    $reportDesignerHref = 'reports.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
    $reportDesignerLink = $renderLink([
        'label' => app_t('Report Designer'),
        'href' => $reportDesignerHref,
        'icon' => 'RP',
        'active' => $activePage === 'reports',
    ], 'site-sidebar__sublink');
    $reportsSubLinksHtml = $reportDesignerLink . $subLinksHtml;

    $visibleModules = app_visible_modules();
    $visibleModuleBySlug = [];
    foreach ($visibleModules as $visibleModule) {
        $visibleSlug = (string) ($visibleModule['slug'] ?? '');
        if ($visibleSlug !== '') {
            $visibleModuleBySlug[$visibleSlug] = $visibleModule;
        }
    }

    $moduleLink = static function (string $slug, array $visibleModuleBySlug, string $companyQuery, string $activePage, ?string $activeModule): string {
        $module = $visibleModuleBySlug[$slug] ?? null;
        if (!is_array($module)) {
            return '';
        }

        $classes = 'site-sidebar__sublink';
        if ($activePage === 'module' && $activeModule === $slug) {
            $classes .= ' is-active';
        }
        $href = 'module.php?module=' . urlencode($slug) . ($companyQuery !== '' ? $companyQuery : '');
        $label = app_escape((string) ($module['title'] ?? $slug));
        $icon = app_escape((string) ($module['icon'] ?? '*'));

        return '<a class="' . app_escape($classes) . '" href="' . app_escape($href) . '">' .
            '<span class="site-sidebar__subicon">' . $icon . '</span>' .
            '<span class="site-sidebar__sublabel">' . $label . '</span>' .
        '</a>';
    };

    $buildModuleLinks = static function (array $slugs) use ($moduleLink, $visibleModuleBySlug, $companyQuery, $activePage, $activeModule): string {
        $html = '';
        foreach ($slugs as $slug) {
            $html .= $moduleLink((string) $slug, $visibleModuleBySlug, $companyQuery, $activePage, $activeModule);
        }
        return $html;
    };

    $workspace = $activePage === 'hr' || $activePage === 'hr_reports'
        ? 'hr'
        : ($activePage === 'student_accounts' || $activePage === 'student_reports'
            ? 'students'
            : ($activePage === 'pos_accounts' || $activePage === 'pos'
                ? 'pos'
                : ($activePage === 'excellence_accounts'
                ? 'excellence'
                : app_workspace_for_module($activeModule))));

    $accountingOperationsSlugs = ['journal_entries', 'sales_invoices', 'purchase_bills', 'expenses', 'sales_returns', 'purchase_returns', 'expense_returns', 'prepaid_expenses', 'prepaid_amortization_runs', 'payments', 'inventory_movements'];
    $accountingSetupSlugs = ['companies', 'users', 'roles', 'role_permissions', 'themes', 'chart_of_accounts', 'financial_statement_groups', 'ecl_report_categories', 'ecl_countries', 'ecl_default_rates', 'ecl_receivable_buckets', 'posting_account_settings', 'contacts', 'products', 'tax_rates', 'bank_accounts', 'cash_accounts', 'cost_centers', 'projects', 'budgets'];
    $fixedAssetSlugs = ['assets', 'depreciation_runs', 'ifrs16_leases', 'ifrs16_lease_schedules'];
    $hrCoreSlugs = ['hr_departments', 'hr_job_titles', 'hr_shifts', 'hr_leave_types', 'hr_salary_components', 'employees', 'hr_employee_contracts'];
    $hrOperationsSlugs = ['hr_attendance', 'hr_leave_requests', 'payroll_runs', 'hr_payroll_items'];
    $studentOperationSlugs = ['student_invoices', 'student_receipts', 'student_refunds'];
    $studentSetupSlugs = ['students', 'student_stages', 'student_grades', 'student_classes', 'student_revenue_types', 'student_fee_plans', 'student_account_settings'];
    $excellenceOperationSlugs = ['excellence_operations', 'excellence_hospital_expenses', 'excellence_admin_expenses', 'excellence_salaries'];
    $excellenceSetupSlugs = ['excellence_hospitals', 'excellence_devices'];
    $posOperationSlugs = ['pos_sessions', 'pos_orders', 'pos_cash_movements'];
    $posSetupSlugs = ['pos_registers', 'products', 'contacts', 'tax_rates', 'bank_accounts', 'cash_accounts', 'cost_centers', 'posting_account_settings'];

    $operationsSlugs = $workspace === 'hr' ? $hrOperationsSlugs : ($workspace === 'students' ? $studentOperationSlugs : ($workspace === 'excellence' ? $excellenceOperationSlugs : ($workspace === 'pos' ? $posOperationSlugs : $accountingOperationsSlugs)));
    $setupSlugs = $workspace === 'hr' ? $hrCoreSlugs : ($workspace === 'students' ? $studentSetupSlugs : ($workspace === 'excellence' ? $excellenceSetupSlugs : ($workspace === 'pos' ? $posSetupSlugs : $accountingSetupSlugs)));

    if ($workspace === 'hr') {
        $hrReportHref = 'hr_reports.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
        $reportsSubLinksHtml = $renderLink([
            'label' => app_t('HR Reports'),
            'href' => $hrReportHref,
            'icon' => 'HR',
            'active' => $activePage === 'hr_reports',
        ], 'site-sidebar__sublink');
    } elseif ($workspace === 'students') {
        $studentReportHref = 'student_reports.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
        $reportsSubLinksHtml = $renderLink([
            'label' => app_t('Student Reports'),
            'href' => $studentReportHref,
            'icon' => 'SR',
            'active' => $activePage === 'student_reports',
        ], 'site-sidebar__sublink');
    } elseif ($workspace === 'excellence') {
        $excellenceHomeHref = 'excellence_accounts.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
        $reportsSubLinksHtml = $renderLink([
            'label' => app_t('EXcellence Dashboard'),
            'href' => $excellenceHomeHref,
            'icon' => 'EX',
            'active' => $activePage === 'excellence_accounts',
        ], 'site-sidebar__sublink');
    } elseif ($workspace === 'pos') {
        $posHomeHref = 'pos_accounts.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
        $reportsSubLinksHtml = $renderLink([
            'label' => app_t('POS Workspace'),
            'href' => $posHomeHref,
            'icon' => 'POS',
            'active' => $activePage === 'pos_accounts',
        ], 'site-sidebar__sublink');
    }

    $posTerminalHtml = '';
    if ($workspace === 'pos' && isset($visibleModuleBySlug['pos_orders'])) {
        $posClasses = 'site-sidebar__sublink' . ($activePage === 'pos' ? ' is-active' : '');
        $posHref = 'pos.php' . ($companyId !== null && $companyId > 0 ? '?company_id=' . urlencode((string) $companyId) : '');
        $posTerminalHtml = '<a class="' . app_escape($posClasses) . '" href="' . app_escape($posHref) . '">' .
            '<span class="site-sidebar__subicon">POS</span>' .
            '<span class="site-sidebar__sublabel">' . app_escape(app_t('POS Terminal')) . '</span>' .
        '</a>';
    }
    $operationsHtml = $posTerminalHtml . $buildModuleLinks($operationsSlugs);
    $setupHtml = $buildModuleLinks($setupSlugs);
    $fixedAssetsHtml = in_array($workspace, ['hr', 'students', 'excellence', 'pos'], true) ? '' : $buildModuleLinks($fixedAssetSlugs);

    $renderGroup = static function (string $label, string $icon, string $accent, string $itemsHtml, bool $active = false): string {
        if ($itemsHtml === '') {
            return '';
        }
        $groupClass = 'site-sidebar__group top-mega-nav__group' . ($active ? ' is-active' : '');
        $buttonClass = 'site-sidebar__link site-sidebar__link--group top-mega-nav__group-button' . ($active ? ' is-active' : '');
        return '<div class="' . app_escape($groupClass) . '" style="--menu-accent:' . app_escape($accent) . '">' .
            '<button class="' . app_escape($buttonClass) . '" type="button">' .
                '<span class="site-sidebar__icon">' . app_escape($icon) . '</span>' .
                '<span class="site-sidebar__label">' . app_escape($label) . '</span>' .
                '<span class="site-sidebar__caret">v</span>' .
            '</button>' .
            '<div class="site-sidebar__sublinks top-mega-nav__dropdown">' . $itemsHtml . '</div>' .
        '</div>';
    };

    $operationsActive = $activePage === 'pos' || ($activePage === 'module' && in_array((string) $activeModule, $operationsSlugs, true));
    $setupActive = $activePage === 'module' && in_array((string) $activeModule, $setupSlugs, true);
    $fixedAssetsActive = $activePage === 'module' && in_array((string) $activeModule, $fixedAssetSlugs, true);

    $menusHtml = '';
    $menusHtml .= $renderGroup(app_t($workspace === 'hr' ? 'HR Operations' : ($workspace === 'students' ? 'Student Operations' : ($workspace === 'excellence' ? 'EXcellence Operations' : ($workspace === 'pos' ? 'POS Operations' : 'Operations')))), $workspace === 'hr' ? 'HO' : ($workspace === 'students' ? 'SO' : ($workspace === 'excellence' ? 'EX' : ($workspace === 'pos' ? 'POS' : 'MV'))), '#72b7b2', $operationsHtml, $operationsActive);
    $menusHtml .= $renderGroup(app_t('Reports'), 'RP', '#f3b23b', $reportsSubLinksHtml, $activePage === 'reports' || $activePage === 'hr_reports' || $activePage === 'student_reports' || $activePage === 'excellence_accounts' || $activePage === 'pos_accounts' || $accountingActive);
    $menusHtml .= $renderGroup(app_t('Fixed Assets'), 'FA', '#ef4444', $fixedAssetsHtml, $fixedAssetsActive);
    $menusHtml .= $renderGroup(app_t($workspace === 'hr' ? 'HR Master Data' : ($workspace === 'students' ? 'Student Setup' : ($workspace === 'excellence' ? 'EXcellence Setup' : ($workspace === 'pos' ? 'POS Setup' : 'System Settings')))), $workspace === 'hr' ? 'HM' : ($workspace === 'students' ? 'SS' : ($workspace === 'excellence' ? 'ES' : ($workspace === 'pos' ? 'PS' : 'ST'))), '#24c45e', $setupHtml, $setupActive);

    $displayNameSafe = app_escape($displayName);
    $companyNameSafe = app_escape((string) $companyName);
    $sidebarCompany = $companyId !== null && $companyId > 0 ? app_company_by_id($companyId) : null;
    $sidebarLogo = app_escape(app_logo_path_for_header($sidebarCompany));
    $controlsHtml = app_ui_controls_html('sidebar');
    $navLabel = app_escape(app_t('Site Navigation'));
    $brandLabel = app_escape((string) app_config('app_name'));
    $dashboardHrefSafe = app_escape($homeHref);
    $userMenuHtml = '';
    if (count(app_current_user_company_options()) > 1) {
        $userMenuHtml .= '<a href="select_company.php">' . app_escape(app_t('Switch Company')) . '</a>';
    }
    $userMenuHtml .= '<a href="change_password.php">' . app_escape(app_t('Change Password')) . '</a>';
    $userMenuHtml .= '<a href="logout.php" class="is-danger">' . app_escape(app_t('Logout')) . '</a>';

    return <<<HTML
<nav class="site-sidebar top-mega-nav screen-only" aria-label="{$navLabel}">
  <div class="top-mega-nav__quick">
    {$quickLinksHtml}
  </div>
  <div class="top-mega-nav__user top-mega-nav__user-menu">
    <button type="button" class="top-mega-nav__user-button">
      <img src="{$sidebarLogo}" alt="Company Logo">
      <span>{$displayNameSafe}</span>
      <span class="top-mega-nav__user-caret">v</span>
    </button>
    <div class="top-mega-nav__user-dropdown">
      <strong>{$companyNameSafe}</strong>
      {$userMenuHtml}
    </div>
  </div>
  <div class="top-mega-nav__menus">
    {$menusHtml}
  </div>
  <div class="top-mega-nav__controls">
    {$controlsHtml}
  </div>
  <a class="top-mega-nav__brand" href="{$dashboardHrefSafe}" title="{$companyNameSafe}">{$brandLabel}</a>
</nav>
HTML;
}
function app_normalize_header_key(string $value): string
{
    $value = trim($value);
    if ($value === '') {
        return '';
    }

    $value = preg_replace('/[^\p{L}\p{N}]+/u', '_', $value) ?? $value;
    $value = trim($value, '_');
    return strtolower($value);
}

function app_module_field_by_name(array $definition, string $fieldName): ?array
{
    foreach ((array) ($definition['fields'] ?? []) as $field) {
        if ((string) ($field['name'] ?? '') === $fieldName) {
            return $field;
        }
    }
    return null;
}

function app_module_table_columns(string $slug): array
{
    $definition = app_module_assert_access($slug);
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table)) {
        throw new RuntimeException('Invalid module table.');
    }

    $pdo = app_pdo();
    $stmt = $pdo->prepare(
        'SELECT COLUMN_NAME, DATA_TYPE
         FROM INFORMATION_SCHEMA.COLUMNS
         WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table
         ORDER BY ORDINAL_POSITION'
    );
    $stmt->execute(['table' => $table]);
    return $stmt->fetchAll() ?: [];
}

function app_module_numeric_columns(string $slug): array
{
    $numericTypes = [
        'int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint',
        'decimal', 'double', 'float', 'real',
    ];

    $columns = [];
    foreach (app_module_table_columns($slug) as $column) {
        $name = (string) ($column['COLUMN_NAME'] ?? '');
        $dataType = strtolower((string) ($column['DATA_TYPE'] ?? ''));
        if ($name === '' || $name === 'id' || $name === 'company_id') {
            continue;
        }
        if (in_array($dataType, $numericTypes, true)) {
            $columns[] = $name;
        }
    }
    return $columns;
}

function app_store_uploaded_file(array $file, string $directory, array $allowedExtensions): string
{
    $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
    if ($error !== UPLOAD_ERR_OK) {
        throw new RuntimeException('File upload failed.');
    }

    $tmpName = (string) ($file['tmp_name'] ?? '');
    if ($tmpName === '' || !is_uploaded_file($tmpName)) {
        throw new RuntimeException('Invalid uploaded file.');
    }

    $original = (string) ($file['name'] ?? 'upload');
    $extension = strtolower(pathinfo($original, PATHINFO_EXTENSION));
    if ($extension === '' || !in_array($extension, $allowedExtensions, true)) {
        throw new RuntimeException('File type is not allowed.');
    }

    $targetDirectory = app_public_path('uploads/' . trim($directory, '\\/'));
    app_ensure_dir($targetDirectory);

    $fileName = date('Ymd_His') . '_' . bin2hex(random_bytes(6)) . '.' . $extension;
    $target = $targetDirectory . DIRECTORY_SEPARATOR . $fileName;
    if (!move_uploaded_file($tmpName, $target)) {
        throw new RuntimeException('Could not save uploaded file.');
    }

    return 'uploads/' . trim($directory, '\\/') . '/' . $fileName;
}

function app_module_import_from_file(string $slug, string $filePath, ?int $companyId): array
{
    $definition = app_module_assert_access($slug);
    $structured = TabularReader::read($filePath);
    $rows = (array) ($structured['rows'] ?? []);

    $aliases = [];
    foreach ((array) ($definition['fields'] ?? []) as $field) {
        $fieldName = (string) ($field['name'] ?? '');
        if ($fieldName === '' || $fieldName === 'password_hash') {
            continue;
        }

        $label = (string) ($field['label'] ?? '');
        foreach ([$fieldName, $label] as $candidate) {
            $key = app_normalize_header_key($candidate);
            if ($key !== '') {
                $aliases[$key] = $fieldName;
            }
        }
    }

    $inserted = 0;
    $skipped = 0;
    $errors = [];
    foreach ($rows as $index => $row) {
        $payload = [];
        foreach ((array) $row as $header => $value) {
            $normalized = app_normalize_header_key((string) $header);
            if (!isset($aliases[$normalized])) {
                continue;
            }
            $payload[$aliases[$normalized]] = $value;
        }

        $hasValues = false;
        foreach ($payload as $value) {
            if (trim((string) $value) !== '') {
                $hasValues = true;
                break;
            }
        }
        if (!$hasValues) {
            $skipped++;
            continue;
        }

        try {
            app_module_save($slug, $payload, $companyId, null);
            $inserted++;
        } catch (Throwable $e) {
            $errors[] = 'Row ' . ($index + 2) . ': ' . $e->getMessage();
        }
    }

    return [
        'inserted' => $inserted,
        'skipped' => $skipped,
        'errors' => $errors,
        'total_rows' => count($rows),
        'header_row' => (int) ($structured['header_row_index'] ?? 1),
    ];
}

function app_report_template_sections(array $template): array
{
    $json = (string) ($template['config_json'] ?? '');
    if (trim($json) === '') {
        return [];
    }

    $decoded = json_decode($json, true);
    if (!is_array($decoded)) {
        return [];
    }

    $sections = $decoded['sections'] ?? null;
    if (!is_array($sections)) {
        return [];
    }

    $normalized = [];
    foreach ($sections as $section) {
        if (!is_array($section)) {
            continue;
        }

        $labelColumn = app_normalize_whitespace((string) ($section['label_column'] ?? ''));
        $module = app_normalize_whitespace((string) ($section['module'] ?? ''));
        $metricColumns = [];
        foreach ((array) ($section['metric_columns'] ?? []) as $metric) {
            $metric = app_normalize_whitespace((string) $metric);
            if ($metric !== '') {
                $metricColumns[] = $metric;
            }
        }

        if ($module === '' || $labelColumn === '' || !$metricColumns) {
            continue;
        }

        $topN = max(1, min(500, app_integer($section['top_n'] ?? 150)));
        $sortMode = app_normalize_whitespace((string) ($section['sort_mode'] ?? 'label_asc'));
        if (!in_array($sortMode, ['label_asc', 'value_desc'], true)) {
            $sortMode = 'label_asc';
        }

        $dateColumn = app_normalize_whitespace((string) ($section['date_column'] ?? ''));
        $dateFrom = app_normalize_whitespace((string) ($section['date_from'] ?? ''));
        $dateTo = app_normalize_whitespace((string) ($section['date_to'] ?? ''));
        $dateFrom = strtotime($dateFrom) !== false ? date('Y-m-d', strtotime($dateFrom)) : '';
        $dateTo = strtotime($dateTo) !== false ? date('Y-m-d', strtotime($dateTo)) : '';
        if ($dateFrom !== '' && $dateTo !== '' && $dateFrom > $dateTo) {
            [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
        }

        $normalized[] = [
            'title' => app_normalize_whitespace((string) ($section['title'] ?? 'Section')),
            'module' => $module,
            'label_column' => $labelColumn,
            'metric_columns' => array_values(array_unique($metricColumns)),
            'chart_type' => app_normalize_whitespace((string) ($section['chart_type'] ?? 'bar')),
            'style' => app_normalize_whitespace((string) ($section['style'] ?? 'classic')),
            'top_n' => $topN,
            'sort_mode' => $sortMode,
            'date_column' => $dateColumn,
            'date_from' => $dateFrom,
            'date_to' => $dateTo,
        ];
    }

    return $normalized;
}

function app_report_series_for_section(array $section, ?int $companyId): array
{
    $moduleSlug = (string) ($section['module'] ?? '');
    $labelColumn = (string) ($section['label_column'] ?? '');
    $metricColumns = (array) ($section['metric_columns'] ?? []);
    $topN = max(1, min(500, app_integer($section['top_n'] ?? 150)));
    $sortMode = app_normalize_whitespace((string) ($section['sort_mode'] ?? 'label_asc'));
    if (!in_array($sortMode, ['label_asc', 'value_desc'], true)) {
        $sortMode = 'label_asc';
    }
    $dateColumn = app_normalize_whitespace((string) ($section['date_column'] ?? ''));
    $dateFrom = app_normalize_whitespace((string) ($section['date_from'] ?? ''));
    $dateTo = app_normalize_whitespace((string) ($section['date_to'] ?? ''));

    if ($moduleSlug === '' || $labelColumn === '' || !$metricColumns) {
        return ['labels' => [], 'series' => []];
    }

    $definition = app_module_definition($moduleSlug);
    $table = (string) ($definition['table'] ?? '');
    if (!app_identifier($table) || !app_identifier($labelColumn)) {
        return ['labels' => [], 'series' => []];
    }

    $validMetricColumns = [];
    foreach ($metricColumns as $column) {
        $column = (string) $column;
        if (app_identifier($column)) {
            $validMetricColumns[] = $column;
        }
    }
    if (!$validMetricColumns) {
        return ['labels' => [], 'series' => []];
    }

    $selectParts = ['`' . $labelColumn . '` AS label_value'];
    foreach ($validMetricColumns as $metric) {
        $selectParts[] = 'SUM(COALESCE(`' . $metric . '`, 0)) AS `' . $metric . '`';
    }

    $sql = 'SELECT ' . implode(', ', $selectParts) . ' FROM `' . $table . '` WHERE 1=1';
    $params = [];
    if (!empty($definition['scoped'])) {
        if ($companyId === null || $companyId <= 0) {
            return ['labels' => [], 'series' => []];
        }
        $sql .= ' AND company_id = :company_id';
        $params['company_id'] = $companyId;
    }
    if ($dateColumn !== '' && app_identifier($dateColumn)) {
        $columnExists = false;
        foreach (app_module_table_columns($moduleSlug) as $columnInfo) {
            if ((string) ($columnInfo['COLUMN_NAME'] ?? '') === $dateColumn) {
                $columnExists = true;
                break;
            }
        }
        if (!$columnExists) {
            $dateColumn = '';
        }
    }

    if ($dateColumn !== '' && app_identifier($dateColumn)) {
        if ($dateFrom !== '' && strtotime($dateFrom) !== false) {
            $dateFrom = date('Y-m-d', strtotime($dateFrom));
            $sql .= ' AND DATE(`' . $dateColumn . '`) >= :section_date_from';
            $params['section_date_from'] = $dateFrom;
        }
        if ($dateTo !== '' && strtotime($dateTo) !== false) {
            $dateTo = date('Y-m-d', strtotime($dateTo));
            $sql .= ' AND DATE(`' . $dateColumn . '`) <= :section_date_to';
            $params['section_date_to'] = $dateTo;
        }
    }

    $sql .= ' GROUP BY `' . $labelColumn . '`';
    if ($sortMode === 'value_desc') {
        $firstMetric = $validMetricColumns[0];
        $sql .= ' ORDER BY SUM(COALESCE(`' . $firstMetric . '`, 0)) DESC, `' . $labelColumn . '` ASC';
    } else {
        $sql .= ' ORDER BY `' . $labelColumn . '` ASC';
    }
    $sql .= ' LIMIT ' . (int) $topN;

    $pdo = app_pdo();
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $rows = $stmt->fetchAll() ?: [];
    if (!$rows) {
        return ['labels' => [], 'series' => []];
    }

    $labels = [];
    $series = [];
    foreach ($validMetricColumns as $metric) {
        $field = app_module_field_by_name($definition, $metric);
        $name = (string) ($field['label'] ?? $metric);
        $series[$metric] = ['name' => $name, 'values' => []];
    }

    foreach ($rows as $row) {
        $label = app_normalize_whitespace((string) ($row['label_value'] ?? ''));
        $labels[] = $label !== '' ? $label : 'N/A';
        foreach ($validMetricColumns as $metric) {
            $series[$metric]['values'][] = app_number($row[$metric] ?? 0);
        }
    }

    return [
        'labels' => $labels,
        'series' => array_values($series),
    ];
}

function app_role_label(string $role): string
{
    return app_t(match ($role) {
        'admin' => 'Admin',
        'auditor' => 'Auditor',
        'accountant' => 'Accountant',
        default => ucfirst($role),
    });
}
