Files
desk-moloni/modules/desk_moloni/helpers/desk_moloni_helper.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

812 lines
24 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Desk-Moloni Helper Functions
*
* Collection of utility functions for the Desk-Moloni module
* to simplify common operations and provide consistent interfaces.
*
* @package DeskMoloni
* @subpackage Helpers
* @version 3.0.0
* @author Descomplicar®
*/
if (!function_exists('desk_moloni_log')) {
/**
* Centralized logging function for Desk-Moloni
*
* @param string $level Log level (error, info, debug, warning)
* @param string $message Log message
* @param array $context Additional context data
* @param string $category Log category (api, sync, oauth, etc.)
*/
function desk_moloni_log($level, $message, $context = [], $category = 'general')
{
$timestamp = date('Y-m-d H:i:s');
$formatted_message = "[$timestamp] [DESK-MOLONI] [$category] [$level] $message";
if (!empty($context)) {
$formatted_message .= ' | Context: ' . json_encode($context, JSON_UNESCAPED_UNICODE);
}
// Log to CodeIgniter log
log_message($level, $formatted_message);
// Also log to custom desk_moloni log file if debug mode is enabled
if (get_option('desk_moloni_debug_mode') == '1') {
$log_dir = APPPATH . '../uploads/desk_moloni/logs/';
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
$log_file = $log_dir . 'desk_moloni_' . date('Y-m-d') . '.log';
file_put_contents($log_file, $formatted_message . PHP_EOL, FILE_APPEND | LOCK_EX);
}
}
}
if (!function_exists('desk_moloni_log_api')) {
/**
* Specialized logging for API calls
*/
function desk_moloni_log_api($endpoint, $method, $data = [], $response = [], $execution_time = null)
{
$context = [
'endpoint' => $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 '<input type="hidden" name="' . $token_name . '" value="' . $token_value . '">';
}
}
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 = '<p><br><strong><b><em><i><u><ul><ol><li><a>';
return strip_tags($html, $allowed_tags);
}
}
if (!function_exists('desk_moloni_generate_reference')) {
/**
* Generate a unique reference for sync operations
*
* @param string $prefix
* @return string
*/
function desk_moloni_generate_reference($prefix = 'DM')
{
return $prefix . date('YmdHis') . sprintf('%04d', mt_rand(0, 9999));
}
}
if (!function_exists('desk_moloni_is_debug_mode')) {
/**
* Check if debug mode is enabled
*
* @return bool
*/
function desk_moloni_is_debug_mode()
{
return get_option('desk_moloni_debug_mode') == '1' || ENVIRONMENT === 'development';
}
}
if (!function_exists('desk_moloni_cache_key')) {
/**
* Generate a cache key for the given parameters
*
* @param string $type
* @param mixed ...$params
* @return string
*/
function desk_moloni_cache_key($type, ...$params)
{
$key_parts = [$type];
foreach ($params as $param) {
$key_parts[] = is_array($param) ? md5(serialize($param)) : (string)$param;
}
return 'desk_moloni:' . implode(':', $key_parts);
}
}
if (!function_exists('desk_moloni_get_cached_data')) {
/**
* Get data from cache
*
* @param string $key
* @param mixed $default
* @return mixed
*/
function desk_moloni_get_cached_data($key, $default = null)
{
if (!get_option('desk_moloni_enable_caching')) {
return $default;
}
$CI = &get_instance();
// Try to get from Redis if available
if (get_option('desk_moloni_enable_redis')) {
$CI->load->library('redis');
$data = $CI->redis->get($key);
return $data !== null ? json_decode($data, true) : $default;
}
// Fallback to file cache
$cache_file = DESK_MOLONI_MODULE_UPLOAD_FOLDER . 'cache/' . md5($key) . '.cache';
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true);
if ($cache_data && $cache_data['expires'] > time()) {
return $cache_data['data'];
}
}
return $default;
}
}
if (!function_exists('desk_moloni_set_cached_data')) {
/**
* Set data in cache
*
* @param string $key
* @param mixed $data
* @param int $ttl
* @return bool
*/
function desk_moloni_set_cached_data($key, $data, $ttl = 3600)
{
if (!get_option('desk_moloni_enable_caching')) {
return false;
}
$CI = &get_instance();
// Try to set in Redis if available
if (get_option('desk_moloni_enable_redis')) {
$CI->load->library('redis');
return $CI->redis->setex($key, $ttl, json_encode($data));
}
// Fallback to file cache
$cache_dir = DESK_MOLONI_MODULE_UPLOAD_FOLDER . 'cache/';
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
$cache_file = $cache_dir . md5($key) . '.cache';
$cache_data = [
'data' => $data,
'expires' => time() + $ttl,
'created' => time()
];
return file_put_contents($cache_file, json_encode($cache_data)) !== false;
}
}
if (!function_exists('desk_moloni_format_date')) {
/**
* Format date for Moloni API
*
* @param string $date
* @param string $format
* @return string
*/
function desk_moloni_format_date($date, $format = 'Y-m-d')
{
if (empty($date)) {
return '';
}
$timestamp = is_numeric($date) ? $date : strtotime($date);
return date($format, $timestamp);
}
}
if (!function_exists('desk_moloni_get_module_version')) {
/**
* Get module version
*
* @return string
*/
function desk_moloni_get_module_version()
{
return defined('DESK_MOLONI_MODULE_VERSION') ? DESK_MOLONI_MODULE_VERSION : DESK_MOLONI_VERSION;
}
}
if (!function_exists('desk_moloni_has_permission')) {
/**
* Check if current user has permission for Desk-Moloni operations
*
* @param string $capability
* @return bool
*/
function desk_moloni_has_permission($capability = 'view')
{
return has_permission('desk_moloni', '', $capability);
}
}
if (!function_exists('desk_moloni_admin_url')) {
/**
* Generate admin URL for Desk-Moloni module
*
* @param string $path
* @return string
*/
function desk_moloni_admin_url($path = '')
{
return admin_url('desk_moloni' . ($path ? '/' . ltrim($path, '/') : ''));
}
}