/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ $endpoint, 'method' => $method, 'request_data' => $data, 'response_data' => $response, 'execution_time_ms' => $execution_time ]; desk_moloni_log('info', "API Call: $method $endpoint", $context, 'api'); } } if (!function_exists('desk_moloni_log_sync')) { /** * Specialized logging for sync operations */ function desk_moloni_log_sync($entity_type, $entity_id, $action, $status, $details = []) { $context = [ 'entity_type' => $entity_type, 'entity_id' => $entity_id, 'action' => $action, 'status' => $status, 'details' => $details ]; $level = ($status === 'success') ? 'info' : 'error'; desk_moloni_log($level, "Sync $action for $entity_type #$entity_id: $status", $context, 'sync'); } } if (!function_exists('desk_moloni_is_enabled')) { /** * Check if Desk-Moloni module is enabled * * @return bool */ function desk_moloni_is_enabled() { return get_option('desk_moloni_enabled') == '1'; } } if (!function_exists('validate_moloni_data')) { /** * Centralized data validation for Moloni data * * @param array $data Data to validate * @param array $rules Validation rules * @param array $messages Custom error messages * @return array ['valid' => bool, 'errors' => array] */ function validate_moloni_data($data, $rules, $messages = []) { $CI = &get_instance(); $CI->load->library('form_validation'); // Clear previous rules $CI->form_validation->reset_validation(); // Set validation rules foreach ($rules as $field => $rule) { $label = isset($messages[$field]) ? $messages[$field] : ucfirst(str_replace('_', ' ', $field)); $CI->form_validation->set_rules($field, $label, $rule, $messages); } $is_valid = $CI->form_validation->run($data); $result = [ 'valid' => $is_valid, 'errors' => [] ]; if (!$is_valid) { $result['errors'] = $CI->form_validation->error_array(); desk_moloni_log('warning', 'Validation failed', [ 'data' => $data, 'errors' => $result['errors'] ], 'validation'); } return $result; } } if (!function_exists('validate_moloni_client')) { /** * Validate client data for Moloni */ function validate_moloni_client($client_data) { $rules = [ 'name' => 'required|max_length[255]', 'vat' => 'required|exact_length[9]|numeric', 'email' => 'valid_email', 'phone' => 'max_length[20]', 'address' => 'max_length[255]', 'city' => 'max_length[100]', 'zip_code' => 'max_length[20]', 'country_id' => 'required|numeric' ]; $messages = [ 'name' => 'Client name', 'vat' => 'VAT number', 'email' => 'Email address', 'phone' => 'Phone number', 'address' => 'Address', 'city' => 'City', 'zip_code' => 'ZIP code', 'country_id' => 'Country' ]; return validate_moloni_data($client_data, $rules, $messages); } } if (!function_exists('validate_moloni_product')) { /** * Validate product data for Moloni */ function validate_moloni_product($product_data) { $rules = [ 'name' => 'required|max_length[255]', 'reference' => 'max_length[50]', 'price' => 'required|numeric|greater_than[0]', 'category_id' => 'numeric', 'unit_id' => 'numeric', 'tax_id' => 'required|numeric', 'stock_enabled' => 'in_list[0,1]' ]; $messages = [ 'name' => 'Product name', 'reference' => 'Product reference', 'price' => 'Product price', 'category_id' => 'Category', 'unit_id' => 'Unit', 'tax_id' => 'Tax', 'stock_enabled' => 'Stock control' ]; return validate_moloni_data($product_data, $rules, $messages); } } if (!function_exists('validate_moloni_invoice')) { /** * Validate invoice data for Moloni */ function validate_moloni_invoice($invoice_data) { $rules = [ 'customer_id' => 'required|numeric', 'document_type' => 'required|in_list[invoices,receipts,bills_of_lading,estimates]', 'products' => 'required|is_array', 'date' => 'required|valid_date[Y-m-d]', 'due_date' => 'valid_date[Y-m-d]', 'notes' => 'max_length[500]' ]; $messages = [ 'customer_id' => 'Customer', 'document_type' => 'Document type', 'products' => 'Products', 'date' => 'Invoice date', 'due_date' => 'Due date', 'notes' => 'Notes' ]; // Validate main invoice data $validation = validate_moloni_data($invoice_data, $rules, $messages); // Validate products if present if (isset($invoice_data['products']) && is_array($invoice_data['products'])) { foreach ($invoice_data['products'] as $index => $product) { $product_rules = [ 'product_id' => 'required|numeric', 'qty' => 'required|numeric|greater_than[0]', 'price' => 'required|numeric|greater_than_equal_to[0]' ]; $product_validation = validate_moloni_data($product, $product_rules); if (!$product_validation['valid']) { $validation['valid'] = false; foreach ($product_validation['errors'] as $field => $error) { $validation['errors']["products[{$index}][{$field}]"] = $error; } } } } return $validation; } } if (!function_exists('validate_moloni_api_response')) { /** * Validate Moloni API response */ function validate_moloni_api_response($response) { if (!is_array($response)) { return [ 'valid' => false, 'errors' => ['Invalid response format'] ]; } // Check for API errors if (isset($response['error'])) { return [ 'valid' => false, 'errors' => [$response['error']] ]; } return [ 'valid' => true, 'errors' => [] ]; } } if (!function_exists('sanitize_moloni_data')) { /** * Sanitize data for Moloni API */ function sanitize_moloni_data($data) { if (is_array($data)) { $sanitized = []; foreach ($data as $key => $value) { $sanitized[$key] = sanitize_moloni_data($value); } return $sanitized; } if (is_string($data)) { // Remove harmful characters, trim whitespace $data = trim($data); $data = filter_var($data, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES); return $data; } return $data; } } if (!function_exists('verify_desk_moloni_csrf')) { /** * Verify CSRF token for Desk-Moloni forms * * @param bool $ajax_response Return JSON response for AJAX requests * @return bool|void True if valid, false or exit if invalid */ function verify_desk_moloni_csrf($ajax_response = false) { $CI = &get_instance(); // Check if CSRF verification is enabled if ($CI->config->item('csrf_protection') !== TRUE) { return true; } $token_name = $CI->security->get_csrf_token_name(); $posted_token = $CI->input->post($token_name); $session_token = $CI->security->get_csrf_hash(); if (!$posted_token || !hash_equals($session_token, $posted_token)) { desk_moloni_log('warning', 'CSRF token validation failed', [ 'ip' => $CI->input->ip_address(), 'user_agent' => $CI->input->user_agent(), 'posted_token' => $posted_token ? 'present' : 'missing', 'session_token' => $session_token ? 'present' : 'missing' ], 'security'); if ($ajax_response) { header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'Invalid security token. Please refresh the page and try again.', 'csrf_error' => true ]); exit; } else { show_error('Invalid security token. Please refresh the page and try again.', 403, 'Security Error'); return false; } } return true; } } if (!function_exists('get_desk_moloni_csrf_data')) { /** * Get CSRF data for JavaScript/AJAX use * * @return array CSRF token name and value */ function get_desk_moloni_csrf_data() { $CI = &get_instance(); return [ 'name' => $CI->security->get_csrf_token_name(), 'value' => $CI->security->get_csrf_hash() ]; } } if (!function_exists('include_csrf_protection')) { /** * Include CSRF protection in forms - shortcut function */ function include_csrf_protection() { $CI = &get_instance(); $token_name = $CI->security->get_csrf_token_name(); $token_value = $CI->security->get_csrf_hash(); echo ''; } } if (!function_exists('desk_moloni_get_api_client')) { /** * Get configured API client instance * * @return object|null */ function desk_moloni_get_api_client() { $CI = &get_instance(); $CI->load->library('desk_moloni/moloni_api_client'); return $CI->moloni_api_client; } } if (!function_exists('desk_moloni_format_currency')) { /** * Format currency value for Moloni API * * @param float $amount * @param string $currency * @return string */ function desk_moloni_format_currency($amount, $currency = 'EUR') { return number_format((float)$amount, 2, '.', ''); } } if (!function_exists('desk_moloni_validate_vat')) { /** * Validate VAT number format * * @param string $vat * @param string $country_code * @return bool */ function desk_moloni_validate_vat($vat, $country_code = 'PT') { $vat = preg_replace('/[^0-9A-Za-z]/', '', $vat); switch (strtoupper($country_code)) { case 'PT': return preg_match('/^[0-9]{9}$/', $vat); case 'ES': return preg_match('/^[A-Z0-9][0-9]{7}[A-Z0-9]$/', $vat); default: return strlen($vat) >= 8 && strlen($vat) <= 12; } } } if (!function_exists('desk_moloni_get_sync_status')) { /** * Get synchronization status for an entity * * @param string $entity_type * @param int $entity_id * @return string|null */ function desk_moloni_get_sync_status($entity_type, $entity_id) { $CI = &get_instance(); $mapping = $CI->db->get_where(db_prefix() . 'desk_moloni_entity_mappings', [ 'entity_type' => $entity_type, 'perfex_id' => $entity_id ])->row(); return $mapping ? $mapping->sync_status : null; } } if (!function_exists('desk_moloni_queue_sync')) { /** * Queue an entity for synchronization * * @param string $entity_type * @param int $entity_id * @param string $action * @param string $priority * @return bool */ function desk_moloni_queue_sync($entity_type, $entity_id, $action = 'sync', $priority = 'normal') { $CI = &get_instance(); $queue_data = [ 'entity_type' => $entity_type, 'entity_id' => $entity_id, 'action' => $action, 'priority' => $priority, 'status' => 'pending', 'created_at' => date('Y-m-d H:i:s'), 'created_by' => get_staff_user_id() ]; return $CI->db->insert(db_prefix() . 'desk_moloni_sync_queue', $queue_data); } } if (!function_exists('desk_moloni_log_error')) { /** * Log an error to the error logs table * * @param string $error_type * @param string $message * @param array $context * @param string $severity * @return bool */ function desk_moloni_log_error($error_type, $message, $context = [], $severity = 'medium') { $CI = &get_instance(); $error_data = [ 'error_type' => $error_type, 'severity' => $severity, 'message' => $message, 'context' => !empty($context) ? json_encode($context) : null, 'created_at' => date('Y-m-d H:i:s'), 'created_by' => get_staff_user_id(), 'ip_address' => $CI->input->ip_address() ]; return $CI->db->insert(db_prefix() . 'desk_moloni_error_logs', $error_data); } } if (!function_exists('desk_moloni_encrypt_data')) { /** * Encrypt sensitive data * * @param mixed $data * @param string $context * @return string|false */ function desk_moloni_encrypt_data($data, $context = '') { if (!get_option('desk_moloni_enable_encryption')) { return $data; } $CI = &get_instance(); $CI->load->library('desk_moloni/Encryption'); try { return $CI->encryption->encrypt($data, $context); } catch (Exception $e) { desk_moloni_log_error('encryption', 'Failed to encrypt data: ' . $e->getMessage()); return false; } } } if (!function_exists('desk_moloni_decrypt_data')) { /** * Decrypt sensitive data * * @param string $encrypted_data * @param string $context * @return mixed|false */ function desk_moloni_decrypt_data($encrypted_data, $context = '') { if (!get_option('desk_moloni_enable_encryption')) { return $encrypted_data; } $CI = &get_instance(); $CI->load->library('desk_moloni/Encryption'); try { return $CI->encryption->decrypt($encrypted_data, $context); } catch (Exception $e) { desk_moloni_log_error('decryption', 'Failed to decrypt data: ' . $e->getMessage()); return false; } } } if (!function_exists('desk_moloni_get_performance_metrics')) { /** * Get performance metrics for a time period * * @param string $metric_type * @param string $start_date * @param string $end_date * @return array */ function desk_moloni_get_performance_metrics($metric_type = null, $start_date = null, $end_date = null) { $CI = &get_instance(); $CI->db->select('metric_name, AVG(metric_value) as avg_value, MAX(metric_value) as max_value, MIN(metric_value) as min_value, COUNT(*) as count') ->from(db_prefix() . 'desk_moloni_performance_metrics'); if ($metric_type) { $CI->db->where('metric_type', $metric_type); } if ($start_date) { $CI->db->where('recorded_at >=', $start_date); } if ($end_date) { $CI->db->where('recorded_at <=', $end_date); } $CI->db->group_by('metric_name'); return $CI->db->get()->result_array(); } } if (!function_exists('desk_moloni_clean_phone_number')) { /** * Clean and format phone number * * @param string $phone * @return string */ function desk_moloni_clean_phone_number($phone) { // Remove all non-digit characters except + $cleaned = preg_replace('/[^+\d]/', '', $phone); // If starts with 00, replace with + if (substr($cleaned, 0, 2) === '00') { $cleaned = '+' . substr($cleaned, 2); } // If Portuguese number without country code, add it if (preg_match('/^[29]\d{8}$/', $cleaned)) { $cleaned = '+351' . $cleaned; } return $cleaned; } } if (!function_exists('desk_moloni_sanitize_html')) { /** * Sanitize HTML content for safe storage * * @param string $html * @return string */ function desk_moloni_sanitize_html($html) { $allowed_tags = '