CI = &get_instance(); $this->CI->load->library('desk_moloni/moloni_oauth'); $this->oauth = $this->CI->moloni_oauth; // Load configuration $this->load_configuration(); } /** * Load API client configuration */ private function load_configuration() { $this->api_timeout = (int)get_option('desk_moloni_api_timeout', 30); $this->connect_timeout = (int)get_option('desk_moloni_connect_timeout', 10); $this->max_retries = (int)get_option('desk_moloni_max_retries', 3); $this->requests_per_minute = (int)get_option('desk_moloni_requests_per_minute', 60); $this->requests_per_hour = (int)get_option('desk_moloni_requests_per_hour', 1000); $this->log_requests = (bool)get_option('desk_moloni_log_requests', true); $this->circuit_breaker_threshold = (int)get_option('desk_moloni_circuit_breaker_threshold', 5); } /** * Configure API client settings * * @param array $config Configuration options * @return bool Configuration success */ public function configure($config = []) { if (isset($config['timeout'])) { $this->api_timeout = (int)$config['timeout']; update_option('desk_moloni_api_timeout', $this->api_timeout); } if (isset($config['max_retries'])) { $this->max_retries = (int)$config['max_retries']; update_option('desk_moloni_max_retries', $this->max_retries); } if (isset($config['rate_limit_per_minute'])) { $this->requests_per_minute = (int)$config['rate_limit_per_minute']; update_option('desk_moloni_requests_per_minute', $this->requests_per_minute); } if (isset($config['rate_limit_per_hour'])) { $this->requests_per_hour = (int)$config['rate_limit_per_hour']; update_option('desk_moloni_requests_per_hour', $this->requests_per_hour); } if (isset($config['log_requests'])) { $this->log_requests = (bool)$config['log_requests']; update_option('desk_moloni_log_requests', $this->log_requests); } log_activity('Desk-Moloni: API client configuration updated'); return true; } // ===================================================== // OAuth 2.0 Token Exchange // ===================================================== /** * Exchange authorization code for access token (OAuth callback) * * @param string $code Authorization code * @param string $redirect_uri Redirect URI * @return array Token response */ public function exchange_token($code, $redirect_uri) { return $this->oauth->handle_callback($code); } // ===================================================== // Customer Management // ===================================================== /** * List customers with pagination * * @param int $company_id Company ID * @param array $options Query options (qty, offset, search) * @return array Customer list */ public function list_customers($company_id, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'qty' => $options['qty'] ?? 50, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('customers/getAll', $params); } /** * Get customer by ID * * @param int $customer_id Customer ID * @param int $company_id Company ID * @return array Customer data */ public function get_customer($customer_id, $company_id) { return $this->make_request('customers/getOne', [ 'customer_id' => $customer_id, 'company_id' => $company_id ]); } /** * Create new customer * * @param array $customer_data Customer data * @return array Created customer */ public function create_customer($customer_data) { $required_fields = ['company_id', 'name', 'vat']; $this->validate_required_fields($customer_data, $required_fields); // Set defaults $customer_data = array_merge([ 'country_id' => 1 // Portugal default ], $customer_data); return $this->make_request('customers/insert', $customer_data); } /** * Update existing customer * * @param int $customer_id Customer ID * @param array $customer_data Updated customer data * @return array Updated customer */ public function update_customer($customer_id, $customer_data) { $required_fields = ['customer_id', 'company_id']; $update_data = array_merge($customer_data, [ 'customer_id' => $customer_id ]); $this->validate_required_fields($update_data, $required_fields); return $this->make_request('customers/update', $update_data); } /** * Search customers * * @param int $company_id Company ID * @param string $search Search term * @param array $options Additional options * @return array Customer search results */ public function search_customers($company_id, $search, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'search' => $search, 'qty' => $options['qty'] ?? 50, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('customers/getAll', $params); } // ===================================================== // Product Management // ===================================================== /** * List products * * @param int $company_id Company ID * @param array $options Query options * @return array Product list */ public function list_products($company_id, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'qty' => $options['qty'] ?? 100, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('products/getAll', $params); } /** * Get product by ID * * @param int $product_id Product ID * @param int $company_id Company ID * @return array Product data */ public function get_product($product_id, $company_id) { return $this->make_request('products/getOne', [ 'product_id' => $product_id, 'company_id' => $company_id ]); } /** * Create new product * * @param array $product_data Product data * @return array Created product */ public function create_product($product_data) { $required_fields = ['company_id', 'name', 'price']; $this->validate_required_fields($product_data, $required_fields); // Set defaults $product_data = array_merge([ 'unit_id' => 1, 'has_stock' => 0 ], $product_data); return $this->make_request('products/insert', $product_data); } /** * Update existing product * * @param int $product_id Product ID * @param array $product_data Updated product data * @return array Updated product */ public function update_product($product_id, $product_data) { $required_fields = ['product_id', 'company_id']; $update_data = array_merge($product_data, [ 'product_id' => $product_id ]); $this->validate_required_fields($update_data, $required_fields); return $this->make_request('products/update', $update_data); } /** * Search products * * @param int $company_id Company ID * @param string $search Search term * @param array $options Additional options * @return array Product search results */ public function search_products($company_id, $search, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'search' => $search, 'qty' => $options['qty'] ?? 100, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('products/getAll', $params); } // ===================================================== // Invoice Management // ===================================================== /** * Create invoice * * @param array $invoice_data Invoice data * @return array Created invoice */ public function create_invoice($invoice_data) { $required_fields = ['company_id', 'customer_id', 'date', 'products']; $this->validate_required_fields($invoice_data, $required_fields); // Validate products if (empty($invoice_data['products']) || !is_array($invoice_data['products'])) { throw new InvalidArgumentException('Invoice must contain at least one product'); } foreach ($invoice_data['products'] as $product) { $this->validate_required_fields($product, ['product_id', 'name', 'qty', 'price']); } return $this->make_request('invoices/insert', $invoice_data); } /** * List invoices with pagination * * @param int $company_id Company ID * @param array $options Query options (qty, offset, search) * @return array Invoice list */ public function list_invoices($company_id, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'qty' => $options['qty'] ?? 50, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('invoices/getAll', $params); } /** * Get invoice by ID * * @param int $invoice_id Invoice ID * @param int $company_id Company ID * @return array Invoice data */ public function get_invoice($invoice_id, $company_id) { return $this->make_request('invoices/getOne', [ 'document_id' => $invoice_id, 'company_id' => $company_id ]); } /** * Update existing invoice * * @param int $invoice_id Invoice ID * @param array $invoice_data Updated invoice data * @return array Updated invoice */ public function update_invoice($invoice_id, $invoice_data) { $required_fields = ['document_id', 'company_id']; $update_data = array_merge($invoice_data, [ 'document_id' => $invoice_id ]); $this->validate_required_fields($update_data, $required_fields); return $this->make_request('invoices/update', $update_data); } /** * Get invoice PDF * * @param int $invoice_id Invoice ID * @param int $company_id Company ID * @return string PDF binary data or PDF URL */ public function get_invoice_pdf($invoice_id, $company_id) { $response = $this->make_request('invoices/getPDFLink', [ 'document_id' => $invoice_id, 'company_id' => $company_id ]); // If response contains PDF URL, download the PDF if (isset($response['url'])) { return $this->download_pdf($response['url']); } return $response; } // ===================================================== // Estimate Management // ===================================================== /** * List estimates * * @param int $company_id Company ID * @param array $options Query options * @return array Estimate list */ public function list_estimates($company_id, $options = []) { $params = array_merge([ 'company_id' => $company_id, 'qty' => $options['qty'] ?? 50, 'offset' => $options['offset'] ?? 0 ], $options); return $this->make_request('estimates/getAll', $params); } /** * Get estimate by ID * * @param int $estimate_id Estimate ID * @param int $company_id Company ID * @return array Estimate data */ public function get_estimate($estimate_id, $company_id) { return $this->make_request('estimates/getOne', [ 'document_id' => $estimate_id, 'company_id' => $company_id ]); } /** * Create estimate * * @param array $estimate_data Estimate data * @return array Created estimate */ public function create_estimate($estimate_data) { $required_fields = ['company_id', 'customer_id', 'date', 'products']; $this->validate_required_fields($estimate_data, $required_fields); // Validate products if (empty($estimate_data['products']) || !is_array($estimate_data['products'])) { throw new InvalidArgumentException('Estimate must contain at least one product'); } foreach ($estimate_data['products'] as $product) { $this->validate_required_fields($product, ['product_id', 'name', 'qty', 'price']); } return $this->make_request('estimates/insert', $estimate_data); } // ===================================================== // Company Management // ===================================================== /** * Get all companies for authenticated user * * @return array Company list */ public function list_companies() { return $this->make_request('companies/getAll'); } /** * Get company by ID * * @param int $company_id Company ID * @return array Company data */ public function get_company($company_id) { return $this->make_request('companies/getOne', [ 'company_id' => $company_id ]); } // ===================================================== // Tax Management // ===================================================== /** * List taxes for company * * @param int $company_id Company ID * @return array Tax list */ public function list_taxes($company_id) { return $this->make_request('taxes/getAll', [ 'company_id' => $company_id ]); } /** * Get tax by ID * * @param int $tax_id Tax ID * @param int $company_id Company ID * @return array Tax data */ public function get_tax($tax_id, $company_id) { return $this->make_request('taxes/getOne', [ 'tax_id' => $tax_id, 'company_id' => $company_id ]); } // ===================================================== // Document Sets Management // ===================================================== /** * List document sets * * @param int $company_id Company ID * @return array Document sets list */ public function list_document_sets($company_id) { return $this->make_request('documentSets/getAll', [ 'company_id' => $company_id ]); } // ===================================================== // Payment Methods Management // ===================================================== /** * List payment methods * * @param int $company_id Company ID * @return array Payment methods list */ public function list_payment_methods($company_id) { return $this->make_request('paymentMethods/getAll', [ 'company_id' => $company_id ]); } // ===================================================== // Product Categories Management // ===================================================== /** * List product categories * * @param int $company_id Company ID * @return array Categories list */ public function list_product_categories($company_id) { return $this->make_request('productCategories/getAll', [ 'company_id' => $company_id ]); } /** * Create product category * * @param array $category_data Category data * @return array Created category */ public function create_product_category($category_data) { $required_fields = ['company_id', 'name']; $this->validate_required_fields($category_data, $required_fields); return $this->make_request('productCategories/insert', $category_data); } // ===================================================== // Units Management // ===================================================== /** * List measurement units * * @param int $company_id Company ID * @return array Units list */ public function list_units($company_id) { return $this->make_request('measurementUnits/getAll', [ 'company_id' => $company_id ]); } // ===================================================== // Countries and Geographical Data // ===================================================== /** * List countries * * @return array Countries list */ public function list_countries() { return $this->make_request('countries/getAll'); } /** * List delivery methods * * @param int $company_id Company ID * @return array Delivery methods list */ public function list_delivery_methods($company_id) { return $this->make_request('deliveryMethods/getAll', [ 'company_id' => $company_id ]); } // ===================================================== // Core Request Handling // ===================================================== /** * Make API request with comprehensive error handling * * @param string $endpoint API endpoint * @param array $params Request parameters * @param string $method HTTP method * @return array Response data * @throws Exception On request failure */ public function make_request($endpoint, $params = [], $method = 'POST') { // Check circuit breaker if ($this->is_circuit_open()) { throw new Exception('Circuit breaker is open - too many recent failures'); } // Apply rate limiting $this->enforce_rate_limits(); // Ensure OAuth connection if (!$this->oauth->is_connected()) { throw new Exception('OAuth not connected'); } $url = $this->api_base_url . $endpoint; $access_token = $this->oauth->get_access_token(); $last_exception = null; for ($attempt = 1; $attempt <= $this->max_retries; $attempt++) { try { $response = $this->execute_request($url, $params, $access_token, $method); // Reset circuit breaker on success $this->circuit_breaker_failures = 0; // Log successful request if ($this->log_requests) { $this->log_api_call($endpoint, $params, $response, null, $attempt); } return $response; } catch (Exception $e) { $last_exception = $e; // Handle authentication errors if ($this->is_auth_error($e) && $attempt === 1) { if ($this->oauth->refresh_access_token()) { $access_token = $this->oauth->get_access_token(); continue; // Retry with new token } } // Check if it's a rate limit error if ($this->is_rate_limit_error($e)) { $wait_time = $this->calculate_rate_limit_wait($e); if ($wait_time > 0 && $wait_time <= 60) { sleep($wait_time); continue; // Retry after waiting } } // Don't retry on client errors (4xx except 401, 429) if ($this->is_client_error($e) && !$this->is_auth_error($e) && !$this->is_rate_limit_error($e)) { break; } // Wait before retry (exponential backoff) if ($attempt < $this->max_retries) { $wait_time = $this->retry_delay * pow(2, $attempt - 1); sleep($wait_time); } } } // Update circuit breaker $this->circuit_breaker_failures++; $this->circuit_breaker_last_failure = time(); // Log failure if ($this->log_errors && $last_exception) { $this->log_api_call($endpoint, $params, null, $last_exception->getMessage(), $this->max_retries); } throw new Exception("API request failed after {$this->max_retries} attempts: " . $last_exception->getMessage()); } /** * Execute HTTP request * * @param string $url Request URL * @param array $params Request parameters * @param string $access_token OAuth access token * @param string $method HTTP method * @return array Response data * @throws Exception On request failure */ private function execute_request($url, $params, $access_token, $method = 'POST') { $ch = curl_init(); $headers = [ 'Authorization: Bearer ' . $access_token, 'Accept: application/json', 'User-Agent: Desk-Moloni/3.0', 'Cache-Control: no-cache' ]; if ($method === 'POST') { $headers[] = 'Content-Type: application/json'; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($params), ]); } else { if (!empty($params)) { $url .= '?' . http_build_query($params); } curl_setopt($ch, CURLOPT_URL, $url); } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->api_timeout, CURLOPT_CONNECTTIMEOUT => $this->connect_timeout, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0, CURLOPT_ENCODING => '', // Enable compression CURLOPT_USERAGENT => 'Desk-Moloni/3.0 (Perfex CRM Integration)' ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); $info = curl_getinfo($ch); curl_close($ch); if ($curl_error) { throw new Exception("CURL Error: {$curl_error}"); } $decoded = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON response from API: ' . json_last_error_msg()); } // Handle HTTP errors if ($http_code >= 400) { $error_msg = $this->extract_error_message($decoded, $http_code); throw new Exception("HTTP {$http_code}: {$error_msg}"); } // Check for API-level errors if (isset($decoded['error'])) { $error_msg = $decoded['error']['message'] ?? $decoded['error']; throw new Exception("Moloni API Error: {$error_msg}"); } return $decoded; } // ===================================================== // Helper Methods // ===================================================== /** * Validate required fields in data array * * @param array $data Data to validate * @param array $required_fields Required field names * @throws InvalidArgumentException If required fields are missing */ private function validate_required_fields($data, $required_fields) { $missing_fields = []; foreach ($required_fields as $field) { if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) { $missing_fields[] = $field; } } if (!empty($missing_fields)) { throw new InvalidArgumentException('Missing required fields: ' . implode(', ', $missing_fields)); } } /** * Check if error is authentication related * * @param Exception $exception Exception to check * @return bool True if auth error */ private function is_auth_error($exception) { $message = strtolower($exception->getMessage()); return strpos($message, 'unauthorized') !== false || strpos($message, 'invalid_token') !== false || strpos($message, 'token_expired') !== false || strpos($message, 'http 401') !== false; } /** * Check if error is rate limit related * * @param Exception $exception Exception to check * @return bool True if rate limit error */ private function is_rate_limit_error($exception) { $message = strtolower($exception->getMessage()); return strpos($message, 'rate limit') !== false || strpos($message, 'too many requests') !== false || strpos($message, 'http 429') !== false; } /** * Check if error is client error (4xx) * * @param Exception $exception Exception to check * @return bool True if client error */ private function is_client_error($exception) { $message = $exception->getMessage(); return preg_match('/HTTP 4\d{2}/', $message); } /** * Calculate wait time for rate limit error * * @param Exception $exception Rate limit exception * @return int Wait time in seconds */ private function calculate_rate_limit_wait($exception) { // Default wait time $default_wait = 60; // Try to extract wait time from error message $message = $exception->getMessage(); if (preg_match('/retry after (\d+)/i', $message, $matches)) { return min((int)$matches[1], 300); // Max 5 minutes } return $default_wait; } /** * Extract error message from API response * * @param array|null $response Decoded response * @param int $http_code HTTP status code * @return string Error message */ private function extract_error_message($response, $http_code) { if (is_array($response)) { if (isset($response['error']['message'])) { return $response['error']['message']; } if (isset($response['error'])) { return is_string($response['error']) ? $response['error'] : 'API Error'; } if (isset($response['message'])) { return $response['message']; } } // Default HTTP error messages $http_messages = [ 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout' ]; return $http_messages[$http_code] ?? "HTTP Error {$http_code}"; } /** * Enforce API rate limits * * @throws Exception If rate limit exceeded */ private function enforce_rate_limits() { $current_time = time(); // Check minute window if ($current_time - $this->minute_window_start >= 60) { $this->minute_window_start = $current_time; $this->request_count_minute = 0; } // Check hour window if ($current_time - $this->hour_window_start >= 3600) { $this->hour_window_start = $current_time; $this->request_count_hour = 0; } // Enforce limits if ($this->request_count_minute >= $this->requests_per_minute) { $wait_time = 60 - ($current_time - $this->minute_window_start); throw new Exception("Rate limit exceeded: {$this->requests_per_minute} requests per minute. Wait {$wait_time} seconds."); } if ($this->request_count_hour >= $this->requests_per_hour) { $wait_time = 3600 - ($current_time - $this->hour_window_start); throw new Exception("Rate limit exceeded: {$this->requests_per_hour} requests per hour. Wait {$wait_time} seconds."); } // Increment counters $this->request_count_minute++; $this->request_count_hour++; } /** * Check if circuit breaker is open * * @return bool True if circuit is open */ private function is_circuit_open() { if ($this->circuit_breaker_failures < $this->circuit_breaker_threshold) { return false; } // Check if timeout has passed if (time() - $this->circuit_breaker_last_failure >= $this->circuit_breaker_timeout) { // Reset circuit breaker $this->circuit_breaker_failures = 0; return false; } return true; } /** * Download PDF from URL * * @param string $url PDF URL * @return string PDF binary data */ private function download_pdf($url) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'Desk-Moloni/3.0 PDF Downloader' ]); $pdf_data = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error || $http_code >= 400) { throw new Exception("PDF download failed: {$error} (HTTP {$http_code})"); } return $pdf_data; } /** * Log API call for debugging and monitoring * * @param string $endpoint API endpoint * @param array $params Request parameters * @param array|null $response Response data * @param string|null $error Error message * @param int $attempt Attempt number */ private function log_api_call($endpoint, $params, $response, $error, $attempt) { if (!$this->log_requests && !$error) { return; } $log_data = [ 'endpoint' => $endpoint, 'params' => $this->log_requests ? json_encode($params) : null, 'response' => ($response && $this->log_responses) ? json_encode($response) : null, 'error' => $error, 'attempt' => $attempt, 'timestamp' => date('Y-m-d H:i:s'), 'user_id' => $this->CI->session->userdata('staff_user_id') ?? 0 ]; // Save to database if model is available if (method_exists($this->CI, 'load')) { try { // Optional: if a dedicated API log model exists if (file_exists(APPPATH . 'modules/desk_moloni/models/Desk_moloni_api_log_model.php')) { $this->CI->load->model('desk_moloni/desk_moloni_api_log_model'); $this->CI->desk_moloni_api_log_model->insert($log_data); } else { // Fallback to activity log $message = "API Call: {$endpoint}"; if ($error) { $message .= " - Error: {$error}"; } log_activity($message); } } catch (Exception $e) { // Fallback to activity log $message = "API Call: {$endpoint}"; if ($error) { $message .= " - Error: {$error}"; } log_activity($message); } } } /** * Get API client status and statistics * * @return array Status information */ public function get_status() { return [ 'oauth_connected' => $this->oauth->is_connected(), 'rate_limits' => [ 'per_minute' => $this->requests_per_minute, 'per_hour' => $this->requests_per_hour, 'current_minute' => $this->request_count_minute, 'current_hour' => $this->request_count_hour ], 'circuit_breaker' => [ 'threshold' => $this->circuit_breaker_threshold, 'failures' => $this->circuit_breaker_failures, 'is_open' => $this->is_circuit_open(), 'last_failure' => $this->circuit_breaker_last_failure ], 'configuration' => [ 'timeout' => $this->api_timeout, 'max_retries' => $this->max_retries, 'base_url' => $this->api_base_url ] ]; } // ===================================================== // Webhook and Event Handling // ===================================================== /** * Process webhook payload from Moloni * * @param array $payload Webhook payload * @param string $signature Webhook signature for verification * @return bool Processing success */ public function process_webhook($payload, $signature = null) { try { // Verify webhook signature if provided if ($signature) { $this->verify_webhook_signature($payload, $signature); } // Validate payload structure if (!isset($payload['event']) || !isset($payload['data'])) { throw new Exception('Invalid webhook payload structure'); } $event = $payload['event']; $data = $payload['data']; // Log webhook received if ($this->log_requests) { $this->log_api_call("webhook:{$event}", $payload, null, null, 1); } // Process based on event type switch ($event) { case 'customer.created': case 'customer.updated': case 'customer.deleted': return $this->handle_customer_webhook($event, $data); case 'product.created': case 'product.updated': case 'product.deleted': return $this->handle_product_webhook($event, $data); case 'invoice.created': case 'invoice.updated': case 'invoice.paid': return $this->handle_invoice_webhook($event, $data); default: // Log unknown event type log_activity("Desk-Moloni: Unknown webhook event type: {$event}"); return true; // Don't fail for unknown events } } catch (Exception $e) { log_activity('Desk-Moloni: Webhook processing failed - ' . $e->getMessage()); return false; } } /** * Verify webhook signature * * @param array $payload Webhook payload * @param string $signature Provided signature * @throws Exception If signature is invalid */ private function verify_webhook_signature($payload, $signature) { $webhook_secret = get_option('desk_moloni_webhook_secret'); if (empty($webhook_secret)) { throw new Exception('Webhook secret not configured'); } $expected_signature = hash_hmac('sha256', json_encode($payload), $webhook_secret); if (!hash_equals($expected_signature, $signature)) { throw new Exception('Invalid webhook signature'); } } /** * Handle customer webhook events * * @param string $event Event type * @param array $data Event data * @return bool Processing success */ private function handle_customer_webhook($event, $data) { // Load client sync service to handle the webhook if (method_exists($this->CI, 'load')) { $this->CI->load->library('desk_moloni/client_sync_service'); switch ($event) { case 'customer.created': case 'customer.updated': return $this->CI->client_sync_service->sync_from_moloni($data['customer_id'], $data['company_id']); case 'customer.deleted': return $this->CI->client_sync_service->handle_moloni_deletion($data['customer_id'], $data['company_id']); default: return true; } } return true; } /** * Handle product webhook events * * @param string $event Event type * @param array $data Event data * @return bool Processing success */ private function handle_product_webhook($event, $data) { // Load product sync service to handle the webhook if (method_exists($this->CI, 'load')) { $this->CI->load->library('desk_moloni/product_sync_service'); switch ($event) { case 'product.created': case 'product.updated': return $this->CI->product_sync_service->sync_from_moloni($data['product_id'], $data['company_id']); case 'product.deleted': return $this->CI->product_sync_service->handle_moloni_deletion($data['product_id'], $data['company_id']); default: return true; } } return true; } /** * Handle invoice webhook events * * @param string $event Event type * @param array $data Event data * @return bool Processing success */ private function handle_invoice_webhook($event, $data) { // Load invoice sync service to handle the webhook if (method_exists($this->CI, 'load')) { $this->CI->load->library('desk_moloni/invoice_sync_service'); switch ($event) { case 'invoice.created': case 'invoice.updated': case 'invoice.paid': return $this->CI->invoice_sync_service->sync_from_moloni($data['document_id'], $data['company_id']); default: return true; } } return true; } // ===================================================== // Bulk Operations // ===================================================== /** * Bulk create customers * * @param array $customers Array of customer data * @return array Results with success/failure for each customer */ public function bulk_create_customers($customers) { $results = []; foreach ($customers as $index => $customer_data) { try { $result = $this->create_customer($customer_data); $results[$index] = [ 'success' => true, 'data' => $result, 'error' => null ]; } catch (Exception $e) { $results[$index] = [ 'success' => false, 'data' => null, 'error' => $e->getMessage() ]; } } return $results; } /** * Bulk create products * * @param array $products Array of product data * @return array Results with success/failure for each product */ public function bulk_create_products($products) { $results = []; foreach ($products as $index => $product_data) { try { $result = $this->create_product($product_data); $results[$index] = [ 'success' => true, 'data' => $result, 'error' => null ]; } catch (Exception $e) { $results[$index] = [ 'success' => false, 'data' => null, 'error' => $e->getMessage() ]; } } return $results; } /** * Bulk update entities with error handling * * @param string $entity_type Entity type (customers, products, invoices) * @param array $updates Array of update data with IDs * @return array Results with success/failure for each update */ public function bulk_update($entity_type, $updates) { $results = []; foreach ($updates as $index => $update_data) { try { switch ($entity_type) { case 'customers': $result = $this->update_customer($update_data['customer_id'], $update_data); break; case 'products': $result = $this->update_product($update_data['product_id'], $update_data); break; case 'invoices': $result = $this->update_invoice($update_data['document_id'], $update_data); break; default: throw new Exception("Unsupported entity type: {$entity_type}"); } $results[$index] = [ 'success' => true, 'data' => $result, 'error' => null ]; } catch (Exception $e) { $results[$index] = [ 'success' => false, 'data' => null, 'error' => $e->getMessage() ]; } } return $results; } /** * Perform comprehensive health check * * @return array Health check results */ public function health_check() { $results = [ 'overall_status' => 'healthy', 'checks' => [], 'timestamp' => date('Y-m-d H:i:s') ]; // OAuth connectivity check try { $oauth_status = $this->oauth->is_connected(); $results['checks']['oauth'] = [ 'status' => $oauth_status ? 'pass' : 'fail', 'message' => $oauth_status ? 'OAuth connected' : 'OAuth not connected' ]; if (!$oauth_status) { $results['overall_status'] = 'unhealthy'; } } catch (Exception $e) { $results['checks']['oauth'] = [ 'status' => 'fail', 'message' => 'OAuth check failed: ' . $e->getMessage() ]; $results['overall_status'] = 'unhealthy'; } // API connectivity check try { $companies = $this->list_companies(); $results['checks']['api_connectivity'] = [ 'status' => 'pass', 'message' => 'API connectivity verified' ]; } catch (Exception $e) { $results['checks']['api_connectivity'] = [ 'status' => 'fail', 'message' => 'API connectivity failed: ' . $e->getMessage() ]; $results['overall_status'] = 'unhealthy'; } // Circuit breaker status $results['checks']['circuit_breaker'] = [ 'status' => $this->is_circuit_open() ? 'warning' : 'pass', 'message' => $this->is_circuit_open() ? 'Circuit breaker is open due to failures' : 'Circuit breaker is closed', 'failures' => $this->circuit_breaker_failures, 'threshold' => $this->circuit_breaker_threshold ]; // Rate limiting status $minute_usage_pct = ($this->request_count_minute / $this->requests_per_minute) * 100; $hour_usage_pct = ($this->request_count_hour / $this->requests_per_hour) * 100; $results['checks']['rate_limits'] = [ 'status' => ($minute_usage_pct > 90 || $hour_usage_pct > 90) ? 'warning' : 'pass', 'message' => sprintf('Rate limit usage: %.1f%% (minute), %.1f%% (hour)', $minute_usage_pct, $hour_usage_pct), 'minute_usage' => $minute_usage_pct, 'hour_usage' => $hour_usage_pct ]; return $results; } }