/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ getMessage()); return false; } } } /** * Enhanced hook registration with existence checks for stability * Prevents fatal errors in case hooks system is not available */ // Initialize module on every load (bulletproof approach) desk_moloni_bulletproof_init(); // Sync hooks with function_exists checks for PHP 8.0+ compatibility if (function_exists('hooks')) { // Customer sync hooks hooks()->add_action('after_client_added', 'desk_moloni_sync_customer_added'); hooks()->add_action('after_client_updated', 'desk_moloni_sync_customer_updated'); // Invoice sync hooks hooks()->add_action('after_invoice_added', 'desk_moloni_sync_invoice_added'); hooks()->add_action('after_invoice_updated', 'desk_moloni_sync_invoice_updated'); // Estimate sync hooks hooks()->add_action('after_estimate_added', 'desk_moloni_sync_estimate_added'); hooks()->add_action('after_estimate_updated', 'desk_moloni_sync_estimate_updated'); // Item/Product sync hooks hooks()->add_action('after_item_added', 'desk_moloni_sync_item_added'); hooks()->add_action('after_item_updated', 'desk_moloni_sync_item_updated'); // Admin interface hooks hooks()->add_action('admin_init', 'desk_moloni_admin_init_hook'); hooks()->add_action('admin_init', 'desk_moloni_init_admin_menu'); // Client portal hooks hooks()->add_action('client_init', 'desk_moloni_client_init_hook'); } /** * BULLETPROOF DATABASE TABLE MANAGEMENT * Ensures all required tables exist without depending on migration system */ if (!function_exists('desk_moloni_ensure_tables_exist')) { function desk_moloni_ensure_tables_exist(): bool { try { $CI = &get_instance(); $CI->load->database(); // Define all required tables $tables = [ 'desk_moloni_sync_queue' => "CREATE TABLE IF NOT EXISTS `" . db_prefix() . "desk_moloni_sync_queue` ( `id` int(11) NOT NULL AUTO_INCREMENT, `entity_type` varchar(50) NOT NULL, `entity_id` int(11) NOT NULL, `perfex_id` int(11) DEFAULT NULL, `moloni_id` int(11) DEFAULT NULL, `action` enum('create','update','delete','sync') NOT NULL DEFAULT 'sync', `direction` enum('perfex_to_moloni','moloni_to_perfex','bidirectional') NOT NULL DEFAULT 'bidirectional', `priority` enum('low','normal','high','critical') NOT NULL DEFAULT 'normal', `status` enum('pending','processing','completed','failed','cancelled') NOT NULL DEFAULT 'pending', `attempts` int(11) NOT NULL DEFAULT 0, `max_attempts` int(11) NOT NULL DEFAULT 3, `data` longtext DEFAULT NULL COMMENT 'JSON data for sync', `error_message` text DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `scheduled_at` timestamp NULL DEFAULT NULL, `started_at` timestamp NULL DEFAULT NULL, `completed_at` timestamp NULL DEFAULT NULL, `created_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_entity_type_id` (`entity_type`, `entity_id`), KEY `idx_status_priority` (`status`, `priority`), KEY `idx_scheduled_at` (`scheduled_at`), KEY `idx_perfex_id` (`perfex_id`), KEY `idx_moloni_id` (`moloni_id`), KEY `idx_created_by` (`created_by`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 'desk_moloni_sync_logs' => "CREATE TABLE IF NOT EXISTS `" . db_prefix() . "desk_moloni_sync_logs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `queue_id` int(11) DEFAULT NULL, `entity_type` varchar(50) NOT NULL, `entity_id` int(11) NOT NULL, `action` varchar(50) NOT NULL, `direction` varchar(50) NOT NULL, `status` enum('started','success','error','warning') NOT NULL, `message` text DEFAULT NULL, `request_data` longtext DEFAULT NULL COMMENT 'JSON request data', `response_data` longtext DEFAULT NULL COMMENT 'JSON response data', `execution_time` decimal(10,4) DEFAULT NULL COMMENT 'Execution time in seconds', `memory_usage` int(11) DEFAULT NULL COMMENT 'Memory usage in bytes', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_queue_id` (`queue_id`), KEY `idx_entity_type_id` (`entity_type`, `entity_id`), KEY `idx_status` (`status`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 'desk_moloni_entity_mappings' => "CREATE TABLE IF NOT EXISTS `" . db_prefix() . "desk_moloni_entity_mappings` ( `id` int(11) NOT NULL AUTO_INCREMENT, `entity_type` varchar(50) NOT NULL, `perfex_id` int(11) NOT NULL, `moloni_id` int(11) NOT NULL, `perfex_hash` varchar(64) DEFAULT NULL COMMENT 'Hash of Perfex entity data', `moloni_hash` varchar(64) DEFAULT NULL COMMENT 'Hash of Moloni entity data', `sync_status` enum('synced','pending','error','conflict') NOT NULL DEFAULT 'synced', `last_sync_at` timestamp NULL DEFAULT NULL, `last_perfex_update` timestamp NULL DEFAULT NULL, `last_moloni_update` timestamp NULL DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `metadata` longtext DEFAULT NULL COMMENT 'Additional mapping metadata JSON', PRIMARY KEY (`id`), UNIQUE KEY `uk_entity_perfex` (`entity_type`, `perfex_id`), UNIQUE KEY `uk_entity_moloni` (`entity_type`, `moloni_id`), KEY `idx_sync_status` (`sync_status`), KEY `idx_last_sync` (`last_sync_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 'desk_moloni_configuration' => "CREATE TABLE IF NOT EXISTS `" . db_prefix() . "desk_moloni_configuration` ( `id` int(11) NOT NULL AUTO_INCREMENT, `config_key` varchar(100) NOT NULL, `config_value` longtext DEFAULT NULL, `config_type` enum('string','integer','boolean','json','encrypted') NOT NULL DEFAULT 'string', `description` text DEFAULT NULL, `category` varchar(50) DEFAULT NULL, `is_system` tinyint(1) NOT NULL DEFAULT 0, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_config_key` (`config_key`), KEY `idx_category` (`category`), KEY `idx_is_system` (`is_system`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 'desk_moloni_api_tokens' => "CREATE TABLE IF NOT EXISTS `" . db_prefix() . "desk_moloni_api_tokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, `token_type` enum('access_token','refresh_token','webhook_token') NOT NULL, `token_value` text NOT NULL COMMENT 'Encrypted token value', `expires_at` timestamp NULL DEFAULT NULL, `company_id` int(11) DEFAULT NULL, `scopes` longtext DEFAULT NULL COMMENT 'JSON scopes', `metadata` longtext DEFAULT NULL COMMENT 'JSON metadata', `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, `last_used_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_token_type` (`token_type`), KEY `idx_company_id` (`company_id`), KEY `idx_active` (`active`), KEY `idx_expires_at` (`expires_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" ]; // Create each table foreach ($tables as $table_name => $sql) { try { $CI->db->query($sql); } catch (Exception $e) { error_log("Error creating table {$table_name}: " . $e->getMessage()); } } return true; } catch (Throwable $e) { error_log("Desk-Moloni table creation error: " . $e->getMessage()); return false; } } } /** * BULLETPROOF CONFIGURATION MANAGEMENT */ if (!function_exists('desk_moloni_ensure_configuration_exists')) { function desk_moloni_ensure_configuration_exists(): bool { try { // Core API Configuration (as module options for backward compatibility) $default_options = [ 'desk_moloni_api_base_url' => 'https://api.moloni.pt/v1/', 'desk_moloni_oauth_base_url' => 'https://www.moloni.pt/v1/', 'desk_moloni_api_timeout' => '30', 'desk_moloni_max_retries' => '3', 'desk_moloni_client_id' => '', 'desk_moloni_client_secret' => '', 'desk_moloni_access_token' => '', 'desk_moloni_refresh_token' => '', 'desk_moloni_token_expires_at' => '', 'desk_moloni_company_id' => '', 'desk_moloni_auto_sync_enabled' => '1', 'desk_moloni_realtime_sync_enabled' => '0', 'desk_moloni_sync_delay' => '300', 'desk_moloni_batch_sync_enabled' => '1', 'desk_moloni_sync_customers' => '1', 'desk_moloni_sync_invoices' => '1', 'desk_moloni_sync_estimates' => '1', 'desk_moloni_sync_credit_notes' => '1', 'desk_moloni_sync_receipts' => '0', 'desk_moloni_sync_products' => '0', 'desk_moloni_enable_monitoring' => '1', 'desk_moloni_enable_performance_tracking' => '1', 'desk_moloni_enable_caching' => '1', 'desk_moloni_cache_ttl' => '3600', 'desk_moloni_enable_encryption' => '1', 'desk_moloni_webhook_signature_verification' => '1', 'desk_moloni_enable_audit_logging' => '1', 'desk_moloni_enable_logging' => '1', 'desk_moloni_log_level' => 'info', 'desk_moloni_log_api_requests' => '0', 'desk_moloni_log_retention_days' => '30', 'desk_moloni_enable_queue' => '1', 'desk_moloni_queue_batch_size' => '10', 'desk_moloni_queue_max_attempts' => '3', 'desk_moloni_queue_retry_delay' => '300', 'desk_moloni_enable_webhooks' => '1', 'desk_moloni_webhook_timeout' => '30', 'desk_moloni_webhook_max_retries' => '3', 'desk_moloni_webhook_secret' => desk_moloni_generate_encryption_key(), 'desk_moloni_enable_client_portal' => '0', 'desk_moloni_client_can_download_pdfs' => '1', 'desk_moloni_continue_on_error' => '1', 'desk_moloni_max_consecutive_errors' => '5', 'desk_moloni_enable_error_notifications' => '1', 'desk_moloni_enable_rate_limiting' => '1', 'desk_moloni_requests_per_minute' => '60', 'desk_moloni_rate_limit_window' => '60', 'desk_moloni_enable_redis' => '0', 'desk_moloni_redis_host' => '127.0.0.1', 'desk_moloni_redis_port' => '6379', 'desk_moloni_redis_database' => '0', 'desk_moloni_module_version' => DESK_MOLONI_VERSION, 'desk_moloni_installation_date' => date('Y-m-d H:i:s'), 'desk_moloni_last_update' => date('Y-m-d H:i:s') ]; // Add each option only if it doesn't exist foreach ($default_options as $key => $value) { if (function_exists('get_option') && function_exists('add_option')) { if (get_option($key) === false) { add_option($key, $value); } } } return true; } catch (Throwable $e) { error_log("Desk-Moloni configuration setup error: " . $e->getMessage()); return false; } } } /** * Generate encryption key helper function */ if (!function_exists('desk_moloni_generate_encryption_key')) { function desk_moloni_generate_encryption_key(int $length = 32): string { try { if ($length < 1) { $length = 32; } return bin2hex(random_bytes($length)); } catch (Exception $e) { // Fallback for older systems return md5(uniqid((string)mt_rand(), true)); } } } /** * BULLETPROOF PERMISSIONS MANAGEMENT */ if (!function_exists('desk_moloni_ensure_permissions_exist')) { function desk_moloni_ensure_permissions_exist(): bool { try { $CI = &get_instance(); $CI->load->database(); // Check if permissions already exist $existing = $CI->db->get_where('tblpermissions', ['name' => 'desk_moloni'])->num_rows(); if ($existing == 0) { $permissions = [ ['name' => 'desk_moloni', 'shortname' => 'view', 'description' => 'View Desk-Moloni module'], ['name' => 'desk_moloni', 'shortname' => 'create', 'description' => 'Create sync tasks and configurations'], ['name' => 'desk_moloni', 'shortname' => 'edit', 'description' => 'Edit configurations and mappings'], ['name' => 'desk_moloni', 'shortname' => 'delete', 'description' => 'Delete sync tasks and clear data'] ]; foreach ($permissions as $permission) { try { $CI->db->insert('tblpermissions', $permission); } catch (Exception $e) { error_log("Error inserting permission: " . $e->getMessage()); } } } return true; } catch (Throwable $e) { error_log("Desk-Moloni permissions setup error: " . $e->getMessage()); return false; } } } /** * Hook functions */ /** * Admin initialization hook with enhanced error handling for PHP 8.0+ */ if (!function_exists('desk_moloni_admin_init_hook')) { function desk_moloni_admin_init_hook(): void { try { $CI = &get_instance(); // Safely load module configuration if (method_exists($CI->load, 'config')) { $config_file = DESK_MOLONI_MODULE_PATH . '/config/config.php'; if (file_exists($config_file)) { $CI->load->config('desk_moloni/config'); } } // Add CSS and JS for admin with file existence checks if (isset($CI->app_css) && method_exists($CI->app_css, 'add')) { $admin_css = DESK_MOLONI_MODULE_PATH . '/assets/css/admin.css'; if (file_exists($admin_css)) { $CI->app_css->add('desk-moloni-admin-css', base_url('modules/desk_moloni/assets/css/admin.css')); } } if (isset($CI->app_scripts) && method_exists($CI->app_scripts, 'add')) { $admin_js = DESK_MOLONI_MODULE_PATH . '/assets/js/admin.js'; if (file_exists($admin_js)) { $CI->app_scripts->add('desk-moloni-admin-js', base_url('modules/desk_moloni/assets/js/admin.js')); } } } catch (Throwable $e) { // Log error but don't break the application error_log("Desk-Moloni admin init error: " . $e->getMessage()); } } } /** * Admin menu initialization with enhanced PHP 8.0+ error handling */ if (!function_exists('desk_moloni_init_admin_menu')) { function desk_moloni_init_admin_menu(): void { try { $CI = &get_instance(); // Check permissions safely with function existence $has_permission = function_exists('has_permission') ? has_permission('desk_moloni', '', 'view') : false; if ($has_permission && isset($CI->app_menu) && method_exists($CI->app_menu, 'add_sidebar_menu_item')) { // Main menu item $CI->app_menu->add_sidebar_menu_item('desk-moloni', [ 'name' => 'Desk-Moloni', 'href' => admin_url('desk_moloni/admin'), 'icon' => 'fa fa-refresh', 'position' => 35, ]); // Define menu items with fallback text in case _l() function is not available $menu_items = [ [ 'slug' => 'desk-moloni-dashboard', 'name' => function_exists('_l') ? _l('Dashboard') : 'Dashboard', 'href' => admin_url('desk_moloni/dashboard'), 'position' => 1, ], [ 'slug' => 'desk-moloni-config', 'name' => function_exists('_l') ? _l('Configuration') : 'Configuration', 'href' => admin_url('desk_moloni/admin/config'), 'position' => 2, ], [ 'slug' => 'desk-moloni-sync', 'name' => function_exists('_l') ? _l('Synchronization') : 'Synchronization', 'href' => admin_url('desk_moloni/admin/manual_sync'), 'position' => 3, ], [ 'slug' => 'desk-moloni-queue', 'name' => function_exists('_l') ? _l('Queue Status') : 'Queue Status', 'href' => admin_url('desk_moloni/queue'), 'position' => 4, ], [ 'slug' => 'desk-moloni-mapping', 'name' => function_exists('_l') ? _l('Mappings') : 'Mappings', 'href' => admin_url('desk_moloni/mapping'), 'position' => 5, ], [ 'slug' => 'desk-moloni-logs', 'name' => function_exists('_l') ? _l('Sync Logs') : 'Sync Logs', 'href' => admin_url('desk_moloni/logs'), 'position' => 6, ], ]; // Add submenu items safely foreach ($menu_items as $item) { if (method_exists($CI->app_menu, 'add_sidebar_children_item')) { $CI->app_menu->add_sidebar_children_item('desk-moloni', $item); } } } } catch (Throwable $e) { // Log error but continue execution error_log("Desk-Moloni menu init error: " . $e->getMessage()); } } } function desk_moloni_client_init_hook(): void { try { $CI = &get_instance(); // Add client portal CSS and JS with file existence checks if (isset($CI->app_css) && method_exists($CI->app_css, 'add')) { $client_css = DESK_MOLONI_MODULE_PATH . '/assets/css/client.css'; if (file_exists($client_css)) { $CI->app_css->add('desk-moloni-client-css', base_url('modules/desk_moloni/assets/css/client.css')); } } if (isset($CI->app_scripts) && method_exists($CI->app_scripts, 'add')) { $client_js = DESK_MOLONI_MODULE_PATH . '/client_portal/dist/js/app.js'; if (file_exists($client_js)) { $CI->app_scripts->add('desk-moloni-client-js', base_url('modules/desk_moloni/client_portal/dist/js/app.js')); } } // Add client portal tab if (function_exists('hooks')) { hooks()->add_action('clients_navigation_end', 'desk_moloni_add_client_tab'); } } catch (Throwable $e) { error_log("Desk-Moloni client init error: " . $e->getMessage()); } } function desk_moloni_add_client_tab(): void { try { $CI = &get_instance(); echo '