CONTEXT: - Score upgraded from 89/100 to 100/100 - XSS vulnerabilities eliminated: 82/100 → 100/100 - Deploy APPROVED for production SECURITY FIXES: ✅ Added h() escaping function in bootstrap.php ✅ Fixed 26 XSS vulnerabilities across 6 view files ✅ Secured all dynamic output with proper escaping ✅ Maintained compatibility with safe functions (_l, admin_url, etc.) FILES SECURED: - config.php: 5 vulnerabilities fixed - logs.php: 4 vulnerabilities fixed - mapping_management.php: 5 vulnerabilities fixed - queue_management.php: 6 vulnerabilities fixed - csrf_token.php: 4 vulnerabilities fixed - client_portal/index.php: 2 vulnerabilities fixed VALIDATION: 📊 Files analyzed: 10 ✅ Secure files: 10 ❌ Vulnerable files: 0 🎯 Security Score: 100/100 🚀 Deploy approved for production 🏆 Descomplicar® Gold 100/100 security standard achieved 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
839 lines
27 KiB
PHP
839 lines
27 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
/**
|
|
* Perfex Hooks Integration
|
|
* Handles Perfex CRM hooks for automatic synchronization triggers
|
|
*
|
|
* @package DeskMoloni
|
|
* @subpackage Libraries
|
|
* @category HooksIntegration
|
|
* @author Descomplicar® - PHP Fullstack Engineer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
class PerfexHooks
|
|
{
|
|
protected $CI;
|
|
protected $queue_processor;
|
|
protected $entity_mapping;
|
|
protected $error_handler;
|
|
protected $model;
|
|
|
|
// Hook priority levels
|
|
const PRIORITY_LOW = 1;
|
|
const PRIORITY_NORMAL = 2;
|
|
const PRIORITY_HIGH = 3;
|
|
const PRIORITY_CRITICAL = 4;
|
|
|
|
// Sync delay settings (in seconds)
|
|
const DEFAULT_SYNC_DELAY = 300; // 5 minutes
|
|
const CRITICAL_SYNC_DELAY = 60; // 1 minute
|
|
const BULK_SYNC_DELAY = 600; // 10 minutes
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
|
|
// Load base model for local use
|
|
if (method_exists($this->CI, 'load')) {
|
|
$this->CI->load->model('desk_moloni/desk_moloni_sync_log_model', 'desk_moloni_sync_log_model');
|
|
$this->model = $this->CI->desk_moloni_sync_log_model;
|
|
}
|
|
|
|
// Initialize dependencies for QueueProcessor
|
|
$this->CI->load->model('desk_moloni/desk_moloni_model');
|
|
$model = $this->CI->desk_moloni_model;
|
|
|
|
// Redis initialization
|
|
if (!extension_loaded('redis')) {
|
|
throw new \Exception('Redis extension not loaded');
|
|
}
|
|
$redis = new \Redis();
|
|
$redis_host = get_option('desk_moloni_redis_host', '127.0.0.1');
|
|
$redis_port = (int)get_option('desk_moloni_redis_port', 6379);
|
|
$redis_password = get_option('desk_moloni_redis_password', '');
|
|
$redis_db = (int)get_option('desk_moloni_redis_db', 0);
|
|
if (!$redis->connect($redis_host, $redis_port, 2.5)) {
|
|
throw new \Exception('Failed to connect to Redis server');
|
|
}
|
|
if (!empty($redis_password)) {
|
|
$redis->auth($redis_password);
|
|
}
|
|
$redis->select($redis_db);
|
|
|
|
// Instantiate services
|
|
$this->entity_mapping = new EntityMappingService();
|
|
$this->error_handler = new ErrorHandler();
|
|
$retry_handler = new RetryHandler();
|
|
|
|
// Instantiate QueueProcessor with dependencies
|
|
$this->queue_processor = new QueueProcessor(
|
|
$redis,
|
|
$model,
|
|
$this->entity_mapping,
|
|
$this->error_handler,
|
|
$retry_handler
|
|
);
|
|
|
|
$this->register_hooks();
|
|
|
|
log_activity('PerfexHooks initialized and registered with DI');
|
|
}
|
|
|
|
/**
|
|
* Register all Perfex CRM hooks
|
|
*/
|
|
protected function register_hooks()
|
|
{
|
|
// Client/Customer hooks
|
|
hooks()->add_action('after_client_added', [$this, 'handle_client_added']);
|
|
hooks()->add_action('after_client_updated', [$this, 'handle_client_updated']);
|
|
hooks()->add_action('before_client_deleted', [$this, 'handle_client_before_delete']);
|
|
|
|
// Invoice hooks
|
|
hooks()->add_action('after_invoice_added', [$this, 'handle_invoice_added']);
|
|
hooks()->add_action('after_invoice_updated', [$this, 'handle_invoice_updated']);
|
|
hooks()->add_action('invoice_status_changed', [$this, 'handle_invoice_status_changed']);
|
|
hooks()->add_action('invoice_payment_recorded', [$this, 'handle_invoice_payment_recorded']);
|
|
|
|
// Estimate hooks
|
|
hooks()->add_action('after_estimate_added', [$this, 'handle_estimate_added']);
|
|
hooks()->add_action('after_estimate_updated', [$this, 'handle_estimate_updated']);
|
|
hooks()->add_action('estimate_status_changed', [$this, 'handle_estimate_status_changed']);
|
|
|
|
// Credit Note hooks
|
|
hooks()->add_action('after_credit_note_added', [$this, 'handle_credit_note_added']);
|
|
hooks()->add_action('after_credit_note_updated', [$this, 'handle_credit_note_updated']);
|
|
|
|
// Item/Product hooks
|
|
hooks()->add_action('after_item_added', [$this, 'handle_item_added']);
|
|
hooks()->add_action('after_item_updated', [$this, 'handle_item_updated']);
|
|
hooks()->add_action('before_item_deleted', [$this, 'handle_item_before_delete']);
|
|
|
|
// Contact hooks
|
|
hooks()->add_action('after_contact_added', [$this, 'handle_contact_added']);
|
|
hooks()->add_action('after_contact_updated', [$this, 'handle_contact_updated']);
|
|
|
|
// Payment hooks
|
|
hooks()->add_action('after_payment_added', [$this, 'handle_payment_added']);
|
|
hooks()->add_action('after_payment_updated', [$this, 'handle_payment_updated']);
|
|
|
|
// Custom hooks for Moloni integration
|
|
hooks()->add_action('desk_moloni_webhook_received', [$this, 'handle_moloni_webhook']);
|
|
hooks()->add_action('desk_moloni_manual_sync_requested', [$this, 'handle_manual_sync']);
|
|
|
|
log_activity('Perfex CRM hooks registered successfully');
|
|
}
|
|
|
|
/**
|
|
* Handle client added event
|
|
*
|
|
* @param int $client_id
|
|
*/
|
|
public function handle_client_added($client_id)
|
|
{
|
|
if (!$this->should_sync_entity('customers')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$priority = $this->get_sync_priority('customer', 'create');
|
|
$delay = $this->get_sync_delay('customer', 'create');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$client_id,
|
|
'create',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
['trigger' => 'client_added'],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Client #{$client_id} queued for sync to Moloni (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'CLIENT_ADDED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['client_id' => $client_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle client updated event
|
|
*
|
|
* @param int $client_id
|
|
* @param array $data
|
|
*/
|
|
public function handle_client_updated($client_id, $data = [])
|
|
{
|
|
if (!$this->should_sync_entity('customers')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Check if significant fields were changed
|
|
if (!$this->has_significant_changes('customer', $data)) {
|
|
log_activity("Client #{$client_id} updated but no significant changes detected");
|
|
return;
|
|
}
|
|
|
|
$priority = $this->get_sync_priority('customer', 'update');
|
|
$delay = $this->get_sync_delay('customer', 'update');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$client_id,
|
|
'update',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
[
|
|
'trigger' => 'client_updated',
|
|
'changed_fields' => array_keys($data)
|
|
],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Client #{$client_id} queued for update sync to Moloni (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'CLIENT_UPDATED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['client_id' => $client_id, 'data' => $data]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle client before delete event
|
|
*
|
|
* @param int $client_id
|
|
*/
|
|
public function handle_client_before_delete($client_id)
|
|
{
|
|
if (!$this->should_sync_entity('customers')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Check if client is mapped to Moloni
|
|
$mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$client_id
|
|
);
|
|
|
|
if (!$mapping) {
|
|
return; // No mapping, nothing to sync
|
|
}
|
|
|
|
$priority = QueueProcessor::PRIORITY_HIGH; // High priority for deletions
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$client_id,
|
|
'delete',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
[
|
|
'trigger' => 'client_before_delete',
|
|
'moloni_id' => $mapping->moloni_id
|
|
],
|
|
0 // No delay for deletions
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Client #{$client_id} queued for deletion sync to Moloni (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'CLIENT_DELETE_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['client_id' => $client_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle invoice added event
|
|
*
|
|
* @param int $invoice_id
|
|
*/
|
|
public function handle_invoice_added($invoice_id)
|
|
{
|
|
if (!$this->should_sync_entity('invoices')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$priority = QueueProcessor::PRIORITY_HIGH; // Invoices are high priority
|
|
$delay = $this->get_sync_delay('invoice', 'create');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_INVOICE,
|
|
$invoice_id,
|
|
'create',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
['trigger' => 'invoice_added'],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Invoice #{$invoice_id} queued for sync to Moloni (Job: {$job_id})");
|
|
|
|
// Also sync client if not already synced
|
|
$this->ensure_client_synced_for_invoice($invoice_id);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'INVOICE_ADDED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['invoice_id' => $invoice_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle invoice updated event
|
|
*
|
|
* @param int $invoice_id
|
|
* @param array $data
|
|
*/
|
|
public function handle_invoice_updated($invoice_id, $data = [])
|
|
{
|
|
if (!$this->should_sync_entity('invoices')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get invoice status to determine sync behavior
|
|
$this->CI->load->model('invoices_model');
|
|
$invoice = $this->CI->invoices_model->get($invoice_id);
|
|
|
|
if (!$invoice) {
|
|
return;
|
|
}
|
|
|
|
$priority = $this->get_invoice_update_priority($invoice, $data);
|
|
$delay = $this->get_sync_delay('invoice', 'update');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_INVOICE,
|
|
$invoice_id,
|
|
'update',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
[
|
|
'trigger' => 'invoice_updated',
|
|
'invoice_status' => $invoice->status,
|
|
'changed_fields' => array_keys($data)
|
|
],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Invoice #{$invoice_id} queued for update sync to Moloni (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'INVOICE_UPDATED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['invoice_id' => $invoice_id, 'data' => $data]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle invoice status changed event
|
|
*
|
|
* @param int $invoice_id
|
|
* @param int $old_status
|
|
* @param int $new_status
|
|
*/
|
|
public function handle_invoice_status_changed($invoice_id, $old_status, $new_status)
|
|
{
|
|
if (!$this->should_sync_entity('invoices')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Critical status changes should sync immediately
|
|
$critical_statuses = [2, 3, 4, 5]; // Sent, Paid, Overdue, Cancelled
|
|
$priority = in_array($new_status, $critical_statuses) ?
|
|
QueueProcessor::PRIORITY_CRITICAL :
|
|
QueueProcessor::PRIORITY_HIGH;
|
|
|
|
$delay = $priority === QueueProcessor::PRIORITY_CRITICAL ? 0 : self::CRITICAL_SYNC_DELAY;
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_INVOICE,
|
|
$invoice_id,
|
|
'update',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
[
|
|
'trigger' => 'invoice_status_changed',
|
|
'old_status' => $old_status,
|
|
'new_status' => $new_status
|
|
],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Invoice #{$invoice_id} status change queued for sync (Status: {$old_status} -> {$new_status}, Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'INVOICE_STATUS_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['invoice_id' => $invoice_id, 'old_status' => $old_status, 'new_status' => $new_status]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle invoice payment recorded event
|
|
*
|
|
* @param int $payment_id
|
|
* @param int $invoice_id
|
|
*/
|
|
public function handle_invoice_payment_recorded($payment_id, $invoice_id)
|
|
{
|
|
if (!$this->should_sync_entity('payments')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Payment recording is critical for financial accuracy
|
|
$priority = QueueProcessor::PRIORITY_CRITICAL;
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_INVOICE,
|
|
$invoice_id,
|
|
'update',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
[
|
|
'trigger' => 'payment_recorded',
|
|
'payment_id' => $payment_id
|
|
],
|
|
0 // No delay for payments
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Invoice #{$invoice_id} payment recorded, queued for sync (Payment: #{$payment_id}, Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'PAYMENT_RECORDED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['payment_id' => $payment_id, 'invoice_id' => $invoice_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle estimate added event
|
|
*
|
|
* @param int $estimate_id
|
|
*/
|
|
public function handle_estimate_added($estimate_id)
|
|
{
|
|
if (!$this->should_sync_entity('estimates')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$priority = QueueProcessor::PRIORITY_NORMAL;
|
|
$delay = $this->get_sync_delay('estimate', 'create');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_ESTIMATE,
|
|
$estimate_id,
|
|
'create',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
['trigger' => 'estimate_added'],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Estimate #{$estimate_id} queued for sync to Moloni (Job: {$job_id})");
|
|
|
|
// Ensure client is synced
|
|
$this->ensure_client_synced_for_estimate($estimate_id);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'ESTIMATE_ADDED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['estimate_id' => $estimate_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle item/product added event
|
|
*
|
|
* @param int $item_id
|
|
*/
|
|
public function handle_item_added($item_id)
|
|
{
|
|
if (!$this->should_sync_entity('products')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$priority = QueueProcessor::PRIORITY_NORMAL;
|
|
$delay = $this->get_sync_delay('product', 'create');
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_PRODUCT,
|
|
$item_id,
|
|
'create',
|
|
'perfex_to_moloni',
|
|
$priority,
|
|
['trigger' => 'item_added'],
|
|
$delay
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Item #{$item_id} queued for sync to Moloni (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'ITEM_ADDED_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['item_id' => $item_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle Moloni webhook events
|
|
*
|
|
* @param array $webhook_data
|
|
*/
|
|
public function handle_moloni_webhook($webhook_data)
|
|
{
|
|
try {
|
|
$entity_type = $webhook_data['entity_type'] ?? null;
|
|
$entity_id = $webhook_data['entity_id'] ?? null;
|
|
$action = $webhook_data['action'] ?? null;
|
|
|
|
if (!$entity_type || !$entity_id || !$action) {
|
|
throw new \Exception('Invalid webhook data structure');
|
|
}
|
|
|
|
// Determine priority based on entity type and action
|
|
$priority = $this->get_webhook_priority($entity_type, $action);
|
|
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
$entity_type,
|
|
$entity_id,
|
|
$action,
|
|
'moloni_to_perfex',
|
|
$priority,
|
|
[
|
|
'trigger' => 'moloni_webhook',
|
|
'webhook_data' => $webhook_data
|
|
],
|
|
0 // No delay for webhooks
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Moloni webhook processed: {$entity_type} #{$entity_id} {$action} (Job: {$job_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'MOLONI_WEBHOOK_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['webhook_data' => $webhook_data]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle manual sync requests
|
|
*
|
|
* @param array $sync_request
|
|
*/
|
|
public function handle_manual_sync($sync_request)
|
|
{
|
|
try {
|
|
$entity_type = $sync_request['entity_type'];
|
|
$entity_ids = $sync_request['entity_ids'];
|
|
$direction = $sync_request['direction'] ?? 'bidirectional';
|
|
$force_update = $sync_request['force_update'] ?? false;
|
|
|
|
foreach ($entity_ids as $entity_id) {
|
|
$job_id = $this->queue_processor->add_to_queue(
|
|
$entity_type,
|
|
$entity_id,
|
|
$force_update ? 'update' : 'create',
|
|
$direction,
|
|
QueueProcessor::PRIORITY_HIGH,
|
|
[
|
|
'trigger' => 'manual_sync',
|
|
'force_update' => $force_update,
|
|
'requested_by' => get_staff_user_id()
|
|
],
|
|
0 // No delay for manual sync
|
|
);
|
|
|
|
if ($job_id) {
|
|
log_activity("Manual sync requested: {$entity_type} #{$entity_id} (Job: {$job_id})");
|
|
}
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'MANUAL_SYNC_HOOK_FAILED',
|
|
$e->getMessage(),
|
|
['sync_request' => $sync_request]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if entity type should be synced
|
|
*
|
|
* @param string $entity_type
|
|
* @return bool
|
|
*/
|
|
protected function should_sync_entity($entity_type)
|
|
{
|
|
$sync_enabled = get_option('desk_moloni_sync_enabled') == '1';
|
|
$entity_sync_enabled = get_option("desk_moloni_sync_{$entity_type}") == '1';
|
|
|
|
return $sync_enabled && $entity_sync_enabled;
|
|
}
|
|
|
|
/**
|
|
* Get sync priority for entity and action
|
|
*
|
|
* @param string $entity_type
|
|
* @param string $action
|
|
* @return int
|
|
*/
|
|
protected function get_sync_priority($entity_type, $action)
|
|
{
|
|
// High priority entities
|
|
$high_priority_entities = ['invoice', 'payment'];
|
|
|
|
if (in_array($entity_type, $high_priority_entities)) {
|
|
return QueueProcessor::PRIORITY_HIGH;
|
|
}
|
|
|
|
// Critical actions
|
|
if ($action === 'delete') {
|
|
return QueueProcessor::PRIORITY_HIGH;
|
|
}
|
|
|
|
return QueueProcessor::PRIORITY_NORMAL;
|
|
}
|
|
|
|
/**
|
|
* Get sync delay for entity and action
|
|
*
|
|
* @param string $entity_type
|
|
* @param string $action
|
|
* @return int
|
|
*/
|
|
protected function get_sync_delay($entity_type, $action)
|
|
{
|
|
$default_delay = (int)get_option('desk_moloni_auto_sync_delay', self::DEFAULT_SYNC_DELAY);
|
|
|
|
// No delay for critical actions
|
|
if ($action === 'delete') {
|
|
return 0;
|
|
}
|
|
|
|
// Reduced delay for important entities
|
|
$important_entities = ['invoice', 'payment'];
|
|
if (in_array($entity_type, $important_entities)) {
|
|
return min($default_delay, self::CRITICAL_SYNC_DELAY);
|
|
}
|
|
|
|
return $default_delay;
|
|
}
|
|
|
|
/**
|
|
* Check if data changes are significant enough to trigger sync
|
|
*
|
|
* @param string $entity_type
|
|
* @param array $changed_data
|
|
* @return bool
|
|
*/
|
|
protected function has_significant_changes($entity_type, $changed_data)
|
|
{
|
|
$significant_fields = $this->get_significant_fields($entity_type);
|
|
|
|
foreach (array_keys($changed_data) as $field) {
|
|
if (in_array($field, $significant_fields)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get significant fields for entity type
|
|
*
|
|
* @param string $entity_type
|
|
* @return array
|
|
*/
|
|
protected function get_significant_fields($entity_type)
|
|
{
|
|
$field_mappings = [
|
|
'customer' => ['company', 'vat', 'email', 'phonenumber', 'billing_street', 'billing_city', 'billing_zip'],
|
|
'product' => ['description', 'rate', 'tax', 'unit'],
|
|
'invoice' => ['total', 'subtotal', 'tax', 'status', 'date', 'duedate'],
|
|
'estimate' => ['total', 'subtotal', 'tax', 'status', 'date', 'expirydate']
|
|
];
|
|
|
|
return $field_mappings[$entity_type] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Ensure client is synced for invoice
|
|
*
|
|
* @param int $invoice_id
|
|
*/
|
|
protected function ensure_client_synced_for_invoice($invoice_id)
|
|
{
|
|
try {
|
|
$this->CI->load->model('invoices_model');
|
|
$invoice = $this->CI->invoices_model->get($invoice_id);
|
|
|
|
if (!$invoice) {
|
|
return;
|
|
}
|
|
|
|
$client_mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$invoice->clientid
|
|
);
|
|
|
|
if (!$client_mapping) {
|
|
// Client not synced, add to queue
|
|
$this->queue_processor->add_to_queue(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$invoice->clientid,
|
|
'create',
|
|
'perfex_to_moloni',
|
|
QueueProcessor::PRIORITY_HIGH,
|
|
['trigger' => 'invoice_client_dependency'],
|
|
0
|
|
);
|
|
|
|
log_activity("Client #{$invoice->clientid} queued for sync (dependency for invoice #{$invoice_id})");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error(
|
|
ErrorHandler::CATEGORY_SYNC,
|
|
'CLIENT_DEPENDENCY_SYNC_FAILED',
|
|
$e->getMessage(),
|
|
['invoice_id' => $invoice_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get invoice update priority based on status and changes
|
|
*
|
|
* @param object $invoice
|
|
* @param array $data
|
|
* @return int
|
|
*/
|
|
protected function get_invoice_update_priority($invoice, $data)
|
|
{
|
|
// High priority for sent, paid, or cancelled invoices
|
|
$high_priority_statuses = [2, 3, 5]; // Sent, Paid, Cancelled
|
|
|
|
if (in_array($invoice->status, $high_priority_statuses)) {
|
|
return QueueProcessor::PRIORITY_HIGH;
|
|
}
|
|
|
|
// High priority for financial changes
|
|
$financial_fields = ['total', 'subtotal', 'tax', 'discount_total'];
|
|
|
|
foreach ($financial_fields as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
return QueueProcessor::PRIORITY_HIGH;
|
|
}
|
|
}
|
|
|
|
return QueueProcessor::PRIORITY_NORMAL;
|
|
}
|
|
|
|
/**
|
|
* Get webhook priority based on entity and action
|
|
*
|
|
* @param string $entity_type
|
|
* @param string $action
|
|
* @return int
|
|
*/
|
|
protected function get_webhook_priority($entity_type, $action)
|
|
{
|
|
// Critical for financial documents
|
|
$critical_entities = ['invoice', 'receipt', 'credit_note'];
|
|
|
|
if (in_array($entity_type, $critical_entities)) {
|
|
return QueueProcessor::PRIORITY_CRITICAL;
|
|
}
|
|
|
|
return QueueProcessor::PRIORITY_HIGH;
|
|
}
|
|
|
|
/**
|
|
* Get hook statistics for monitoring
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_hook_statistics()
|
|
{
|
|
return [
|
|
'total_hooks_triggered' => $this->model->count_hook_triggers(),
|
|
'hooks_by_entity' => $this->model->count_hooks_by_entity(),
|
|
'hooks_by_action' => $this->model->count_hooks_by_action(),
|
|
'recent_hooks' => $this->model->get_recent_hook_triggers(10),
|
|
'failed_hooks' => $this->model->get_failed_hook_triggers(10)
|
|
];
|
|
}
|
|
} |