- 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.
789 lines
30 KiB
PHP
789 lines
30 KiB
PHP
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Estimate Synchronization Service
|
|
* Enhanced bidirectional sync service for estimates between Perfex CRM and Moloni ERP
|
|
*
|
|
* @package DeskMoloni
|
|
* @subpackage Libraries
|
|
* @category EstimateSync
|
|
* @author Descomplicar® - PHP Fullstack Engineer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
namespace DeskMoloni\Libraries;
|
|
|
|
use DeskMoloni\Libraries\EntityMappingService;
|
|
use DeskMoloni\Libraries\ErrorHandler;
|
|
use DeskMoloni\Libraries\MoloniApiClient;
|
|
use DeskMoloni\Libraries\ClientSyncService;
|
|
use DeskMoloni\Libraries\ProductSyncService;
|
|
|
|
class EstimateSyncService
|
|
{
|
|
protected $CI;
|
|
protected $api_client;
|
|
protected $entity_mapping;
|
|
protected $error_handler;
|
|
protected $model;
|
|
protected $client_sync;
|
|
protected $product_sync;
|
|
|
|
// Estimate status mapping
|
|
const STATUS_DRAFT = 1;
|
|
const STATUS_SENT = 2;
|
|
const STATUS_DECLINED = 3;
|
|
const STATUS_ACCEPTED = 4;
|
|
const STATUS_EXPIRED = 5;
|
|
|
|
// Moloni document types for estimates
|
|
const MOLONI_DOC_TYPE_QUOTE = 'quote';
|
|
const MOLONI_DOC_TYPE_PROFORMA = 'proforma';
|
|
const MOLONI_DOC_TYPE_BUDGET = 'budget';
|
|
|
|
// Conflict resolution strategies
|
|
const CONFLICT_STRATEGY_MANUAL = 'manual';
|
|
const CONFLICT_STRATEGY_NEWEST = 'newest';
|
|
const CONFLICT_STRATEGY_PERFEX_WINS = 'perfex_wins';
|
|
const CONFLICT_STRATEGY_MOLONI_WINS = 'moloni_wins';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
$this->CI->load->model('desk_moloni_model');
|
|
$this->CI->load->model('estimates_model');
|
|
|
|
$this->model = $this->CI->desk_moloni_model;
|
|
$this->api_client = new MoloniApiClient();
|
|
$this->entity_mapping = new EntityMappingService();
|
|
$this->error_handler = new ErrorHandler();
|
|
$this->client_sync = new ClientSyncService();
|
|
$this->product_sync = new ProductSyncService();
|
|
|
|
log_activity('EstimateSyncService initialized');
|
|
}
|
|
|
|
/**
|
|
* Sync estimate from Perfex to Moloni
|
|
*
|
|
* @param int $perfex_estimate_id
|
|
* @param bool $force_update
|
|
* @param array $additional_data
|
|
* @return array
|
|
*/
|
|
public function sync_perfex_to_moloni($perfex_estimate_id, $force_update = false, $additional_data = [])
|
|
{
|
|
$start_time = microtime(true);
|
|
|
|
try {
|
|
// Get Perfex estimate data
|
|
$perfex_estimate = $this->get_perfex_estimate($perfex_estimate_id);
|
|
if (!$perfex_estimate) {
|
|
throw new \Exception("Perfex estimate ID {$perfex_estimate_id} not found");
|
|
}
|
|
|
|
// Check existing mapping
|
|
$mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_ESTIMATE,
|
|
$perfex_estimate_id
|
|
);
|
|
|
|
// Validate sync conditions
|
|
if (!$this->should_sync_to_moloni($mapping, $force_update)) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Estimate already synced and up to date',
|
|
'mapping_id' => $mapping ? $mapping->id : null,
|
|
'moloni_estimate_id' => $mapping ? $mapping->moloni_id : null,
|
|
'skipped' => true
|
|
];
|
|
}
|
|
|
|
// Check for conflicts if mapping exists
|
|
if ($mapping && !$force_update) {
|
|
$conflict_check = $this->check_sync_conflicts($mapping);
|
|
if ($conflict_check['has_conflict']) {
|
|
return $this->handle_sync_conflict($mapping, $conflict_check);
|
|
}
|
|
}
|
|
|
|
// Ensure client is synced first
|
|
$client_result = $this->ensure_client_synced($perfex_estimate);
|
|
if (!$client_result['success']) {
|
|
throw new \Exception("Failed to sync client: " . $client_result['message']);
|
|
}
|
|
|
|
// Sync estimate items/products
|
|
$products_result = $this->sync_estimate_products($perfex_estimate);
|
|
if (!$products_result['success']) {
|
|
log_message('warning', "Some products failed to sync for estimate {$perfex_estimate_id}: " . $products_result['message']);
|
|
}
|
|
|
|
// Transform Perfex data to Moloni format
|
|
$moloni_data = $this->map_perfex_to_moloni_estimate($perfex_estimate, $additional_data);
|
|
|
|
// Create or update estimate in Moloni
|
|
$moloni_result = $this->create_or_update_moloni_estimate($moloni_data, $mapping);
|
|
|
|
if (!$moloni_result['success']) {
|
|
throw new \Exception("Moloni API error: " . $moloni_result['message']);
|
|
}
|
|
|
|
$moloni_estimate_id = $moloni_result['estimate_id'];
|
|
$action = $moloni_result['action'];
|
|
|
|
// Update or create mapping
|
|
$mapping_id = $this->update_or_create_mapping(
|
|
EntityMappingService::ENTITY_ESTIMATE,
|
|
$perfex_estimate_id,
|
|
$moloni_estimate_id,
|
|
EntityMappingService::DIRECTION_PERFEX_TO_MOLONI,
|
|
$mapping
|
|
);
|
|
|
|
// Log sync activity
|
|
$execution_time = microtime(true) - $start_time;
|
|
$this->log_sync_activity([
|
|
'entity_type' => 'estimate',
|
|
'entity_id' => $perfex_estimate_id,
|
|
'action' => $action,
|
|
'direction' => 'perfex_to_moloni',
|
|
'status' => 'success',
|
|
'mapping_id' => $mapping_id,
|
|
'request_data' => json_encode($moloni_data),
|
|
'response_data' => json_encode($moloni_result),
|
|
'processing_time' => $execution_time,
|
|
'perfex_data_hash' => $this->calculate_data_hash($perfex_estimate),
|
|
'moloni_data_hash' => $this->calculate_data_hash($moloni_result['data'] ?? [])
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "Estimate {$action}d successfully in Moloni",
|
|
'mapping_id' => $mapping_id,
|
|
'moloni_estimate_id' => $moloni_estimate_id,
|
|
'action' => $action,
|
|
'execution_time' => $execution_time,
|
|
'data_changes' => $this->detect_data_changes($perfex_estimate, $moloni_result['data'] ?? [])
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->handle_sync_error($e, [
|
|
'entity_type' => 'estimate',
|
|
'entity_id' => $perfex_estimate_id,
|
|
'direction' => 'perfex_to_moloni',
|
|
'execution_time' => microtime(true) - $start_time,
|
|
'mapping' => $mapping ?? null
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync estimate from Moloni to Perfex
|
|
*
|
|
* @param int $moloni_estimate_id
|
|
* @param bool $force_update
|
|
* @param array $additional_data
|
|
* @return array
|
|
*/
|
|
public function sync_moloni_to_perfex($moloni_estimate_id, $force_update = false, $additional_data = [])
|
|
{
|
|
$start_time = microtime(true);
|
|
|
|
try {
|
|
// Get Moloni estimate data
|
|
$moloni_response = $this->api_client->get_estimate($moloni_estimate_id);
|
|
if (!$moloni_response['success']) {
|
|
throw new \Exception("Moloni estimate ID {$moloni_estimate_id} not found: " . $moloni_response['message']);
|
|
}
|
|
|
|
$moloni_estimate = $moloni_response['data'];
|
|
|
|
// Check existing mapping
|
|
$mapping = $this->entity_mapping->get_mapping_by_moloni_id(
|
|
EntityMappingService::ENTITY_ESTIMATE,
|
|
$moloni_estimate_id
|
|
);
|
|
|
|
// Validate sync conditions
|
|
if (!$this->should_sync_to_perfex($mapping, $force_update)) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Estimate already synced and up to date',
|
|
'mapping_id' => $mapping ? $mapping->id : null,
|
|
'perfex_estimate_id' => $mapping ? $mapping->perfex_id : null,
|
|
'skipped' => true
|
|
];
|
|
}
|
|
|
|
// Check for conflicts if mapping exists
|
|
if ($mapping && !$force_update) {
|
|
$conflict_check = $this->check_sync_conflicts($mapping);
|
|
if ($conflict_check['has_conflict']) {
|
|
return $this->handle_sync_conflict($mapping, $conflict_check);
|
|
}
|
|
}
|
|
|
|
// Ensure client is synced first
|
|
$client_result = $this->ensure_moloni_client_synced($moloni_estimate);
|
|
if (!$client_result['success']) {
|
|
throw new \Exception("Failed to sync client: " . $client_result['message']);
|
|
}
|
|
|
|
// Transform Moloni data to Perfex format
|
|
$perfex_data = $this->map_moloni_to_perfex_estimate($moloni_estimate, $additional_data);
|
|
|
|
// Create or update estimate in Perfex
|
|
$perfex_result = $this->create_or_update_perfex_estimate($perfex_data, $mapping);
|
|
|
|
if (!$perfex_result['success']) {
|
|
throw new \Exception("Perfex CRM error: " . $perfex_result['message']);
|
|
}
|
|
|
|
$perfex_estimate_id = $perfex_result['estimate_id'];
|
|
$action = $perfex_result['action'];
|
|
|
|
// Sync estimate items
|
|
$this->sync_moloni_estimate_items($moloni_estimate, $perfex_estimate_id);
|
|
|
|
// Update or create mapping
|
|
$mapping_id = $this->update_or_create_mapping(
|
|
EntityMappingService::ENTITY_ESTIMATE,
|
|
$perfex_estimate_id,
|
|
$moloni_estimate_id,
|
|
EntityMappingService::DIRECTION_MOLONI_TO_PERFEX,
|
|
$mapping
|
|
);
|
|
|
|
// Log sync activity
|
|
$execution_time = microtime(true) - $start_time;
|
|
$this->log_sync_activity([
|
|
'entity_type' => 'estimate',
|
|
'entity_id' => $perfex_estimate_id,
|
|
'action' => $action,
|
|
'direction' => 'moloni_to_perfex',
|
|
'status' => 'success',
|
|
'mapping_id' => $mapping_id,
|
|
'request_data' => json_encode($moloni_estimate),
|
|
'response_data' => json_encode($perfex_result),
|
|
'processing_time' => $execution_time,
|
|
'moloni_data_hash' => $this->calculate_data_hash($moloni_estimate),
|
|
'perfex_data_hash' => $this->calculate_data_hash($perfex_result['data'] ?? [])
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "Estimate {$action}d successfully in Perfex",
|
|
'mapping_id' => $mapping_id,
|
|
'perfex_estimate_id' => $perfex_estimate_id,
|
|
'action' => $action,
|
|
'execution_time' => $execution_time,
|
|
'data_changes' => $this->detect_data_changes($moloni_estimate, $perfex_result['data'] ?? [])
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->handle_sync_error($e, [
|
|
'entity_type' => 'estimate',
|
|
'entity_id' => $moloni_estimate_id,
|
|
'direction' => 'moloni_to_perfex',
|
|
'execution_time' => microtime(true) - $start_time,
|
|
'mapping' => $mapping ?? null
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for synchronization conflicts
|
|
*
|
|
* @param object $mapping
|
|
* @return array
|
|
*/
|
|
public function check_sync_conflicts($mapping)
|
|
{
|
|
try {
|
|
$conflicts = [];
|
|
|
|
// Get current data from both systems
|
|
$perfex_estimate = $this->get_perfex_estimate($mapping->perfex_id);
|
|
$moloni_response = $this->api_client->get_estimate($mapping->moloni_id);
|
|
|
|
if (!$perfex_estimate || !$moloni_response['success']) {
|
|
return ['has_conflict' => false];
|
|
}
|
|
|
|
$moloni_estimate = $moloni_response['data'];
|
|
|
|
// Check modification timestamps
|
|
$perfex_modified = $this->get_perfex_modification_time($mapping->perfex_id);
|
|
$moloni_modified = $this->get_moloni_modification_time($mapping->moloni_id);
|
|
$last_sync = max(
|
|
strtotime($mapping->last_sync_perfex ?: '1970-01-01'),
|
|
strtotime($mapping->last_sync_moloni ?: '1970-01-01')
|
|
);
|
|
|
|
$perfex_changed_after_sync = $perfex_modified > $last_sync;
|
|
$moloni_changed_after_sync = $moloni_modified > $last_sync;
|
|
|
|
if ($perfex_changed_after_sync && $moloni_changed_after_sync) {
|
|
// Both sides modified since last sync - check for field conflicts
|
|
$field_conflicts = $this->detect_estimate_field_conflicts($perfex_estimate, $moloni_estimate);
|
|
|
|
if (!empty($field_conflicts)) {
|
|
$conflicts = [
|
|
'type' => 'data_conflict',
|
|
'message' => 'Both systems have been modified since last sync',
|
|
'field_conflicts' => $field_conflicts,
|
|
'perfex_modified' => date('Y-m-d H:i:s', $perfex_modified),
|
|
'moloni_modified' => date('Y-m-d H:i:s', $moloni_modified),
|
|
'last_sync' => $mapping->last_sync_perfex ?: $mapping->last_sync_moloni
|
|
];
|
|
}
|
|
}
|
|
|
|
// Check for status conflicts
|
|
if ($this->has_status_conflicts($perfex_estimate, $moloni_estimate)) {
|
|
$conflicts['status_conflict'] = [
|
|
'perfex_status' => $perfex_estimate['status'],
|
|
'moloni_status' => $moloni_estimate['status'],
|
|
'message' => 'Estimate status differs between systems'
|
|
];
|
|
}
|
|
|
|
return [
|
|
'has_conflict' => !empty($conflicts),
|
|
'conflict_details' => $conflicts
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error_handler->log_error('sync', 'ESTIMATE_CONFLICT_CHECK_FAILED', $e->getMessage(), [
|
|
'mapping_id' => $mapping->id
|
|
]);
|
|
|
|
return ['has_conflict' => false];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map Perfex estimate to Moloni format
|
|
*
|
|
* @param array $perfex_estimate
|
|
* @param array $additional_data
|
|
* @return array
|
|
*/
|
|
protected function map_perfex_to_moloni_estimate($perfex_estimate, $additional_data = [])
|
|
{
|
|
// Get client mapping
|
|
$client_mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$perfex_estimate['clientid']
|
|
);
|
|
|
|
if (!$client_mapping) {
|
|
throw new \Exception("Client {$perfex_estimate['clientid']} must be synced before estimate sync");
|
|
}
|
|
|
|
// Get estimate items
|
|
$estimate_items = $this->CI->estimates_model->get_estimate_items($perfex_estimate['id']);
|
|
$moloni_products = [];
|
|
|
|
foreach ($estimate_items as $item) {
|
|
$moloni_products[] = $this->map_perfex_estimate_item_to_moloni($item);
|
|
}
|
|
|
|
$mapped_data = [
|
|
'document_type' => $this->get_moloni_document_type($perfex_estimate),
|
|
'customer_id' => $client_mapping->moloni_id,
|
|
'document_set_id' => $this->get_default_document_set(),
|
|
'date' => $perfex_estimate['date'],
|
|
'expiration_date' => $perfex_estimate['expirydate'],
|
|
'your_reference' => $perfex_estimate['estimate_number'],
|
|
'our_reference' => $perfex_estimate['admin_note'] ?? '',
|
|
'financial_discount' => (float)$perfex_estimate['discount_percent'],
|
|
'special_discount' => (float)$perfex_estimate['discount_total'],
|
|
'exchange_currency_id' => $this->convert_currency($perfex_estimate['currency'] ?? get_base_currency()->id),
|
|
'exchange_rate' => 1.0,
|
|
'notes' => $this->build_estimate_notes($perfex_estimate),
|
|
'status' => $this->convert_perfex_status_to_moloni($perfex_estimate['status']),
|
|
'products' => $moloni_products,
|
|
'valid_until' => $perfex_estimate['expirydate']
|
|
];
|
|
|
|
// Add tax summary
|
|
$mapped_data['tax_exemption'] = $this->get_tax_exemption_reason($perfex_estimate);
|
|
|
|
// Apply additional data overrides
|
|
$mapped_data = array_merge($mapped_data, $additional_data);
|
|
|
|
// Clean and validate data
|
|
return $this->clean_moloni_estimate_data($mapped_data);
|
|
}
|
|
|
|
/**
|
|
* Map Moloni estimate to Perfex format
|
|
*
|
|
* @param array $moloni_estimate
|
|
* @param array $additional_data
|
|
* @return array
|
|
*/
|
|
protected function map_moloni_to_perfex_estimate($moloni_estimate, $additional_data = [])
|
|
{
|
|
// Get client mapping
|
|
$client_mapping = $this->entity_mapping->get_mapping_by_moloni_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$moloni_estimate['customer_id']
|
|
);
|
|
|
|
if (!$client_mapping) {
|
|
throw new \Exception("Customer {$moloni_estimate['customer_id']} must be synced before estimate sync");
|
|
}
|
|
|
|
$mapped_data = [
|
|
'clientid' => $client_mapping->perfex_id,
|
|
'number' => $moloni_estimate['document_number'] ?? '',
|
|
'date' => $moloni_estimate['date'],
|
|
'expirydate' => $moloni_estimate['valid_until'] ?? $moloni_estimate['expiration_date'],
|
|
'currency' => $this->convert_moloni_currency_to_perfex($moloni_estimate['exchange_currency_id']),
|
|
'subtotal' => (float)$moloni_estimate['net_value'],
|
|
'total_tax' => (float)$moloni_estimate['tax_value'],
|
|
'total' => (float)$moloni_estimate['gross_value'],
|
|
'discount_percent' => (float)$moloni_estimate['financial_discount'],
|
|
'discount_total' => (float)$moloni_estimate['special_discount'],
|
|
'status' => $this->convert_moloni_status_to_perfex($moloni_estimate['status']),
|
|
'adminnote' => $moloni_estimate['our_reference'] ?? '',
|
|
'clientnote' => $moloni_estimate['notes'] ?? ''
|
|
];
|
|
|
|
// Apply additional data overrides
|
|
$mapped_data = array_merge($mapped_data, $additional_data);
|
|
|
|
// Clean and validate data
|
|
return $this->clean_perfex_estimate_data($mapped_data);
|
|
}
|
|
|
|
/**
|
|
* Map Perfex estimate item to Moloni product format
|
|
*
|
|
* @param array $item
|
|
* @return array
|
|
*/
|
|
protected function map_perfex_estimate_item_to_moloni($item)
|
|
{
|
|
// Try to get product mapping
|
|
$product_mapping = null;
|
|
if (!empty($item['rel_id']) && $item['rel_type'] === 'item') {
|
|
$product_mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_PRODUCT,
|
|
$item['rel_id']
|
|
);
|
|
}
|
|
|
|
return [
|
|
'product_id' => $product_mapping ? $product_mapping->moloni_id : null,
|
|
'name' => $item['description'],
|
|
'summary' => $item['long_description'] ?? '',
|
|
'qty' => (float)$item['qty'],
|
|
'price' => (float)$item['rate'],
|
|
'discount' => 0,
|
|
'order' => (int)$item['item_order'],
|
|
'exemption_reason' => '',
|
|
'taxes' => $this->get_item_tax_data($item)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Ensure client is synced before estimate sync
|
|
*
|
|
* @param array $perfex_estimate
|
|
* @return array
|
|
*/
|
|
protected function ensure_client_synced($perfex_estimate)
|
|
{
|
|
$mapping = $this->entity_mapping->get_mapping_by_perfex_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$perfex_estimate['clientid']
|
|
);
|
|
|
|
if (!$mapping) {
|
|
// Sync client first
|
|
return $this->client_sync->sync_perfex_to_moloni($perfex_estimate['clientid'], false);
|
|
}
|
|
|
|
return ['success' => true, 'message' => 'Client already synced'];
|
|
}
|
|
|
|
/**
|
|
* Ensure Moloni client is synced
|
|
*
|
|
* @param array $moloni_estimate
|
|
* @return array
|
|
*/
|
|
protected function ensure_moloni_client_synced($moloni_estimate)
|
|
{
|
|
$mapping = $this->entity_mapping->get_mapping_by_moloni_id(
|
|
EntityMappingService::ENTITY_CUSTOMER,
|
|
$moloni_estimate['customer_id']
|
|
);
|
|
|
|
if (!$mapping) {
|
|
// Sync client first
|
|
return $this->client_sync->sync_moloni_to_perfex($moloni_estimate['customer_id'], false);
|
|
}
|
|
|
|
return ['success' => true, 'message' => 'Client already synced'];
|
|
}
|
|
|
|
/**
|
|
* Sync estimate products
|
|
*
|
|
* @param array $perfex_estimate
|
|
* @return array
|
|
*/
|
|
protected function sync_estimate_products($perfex_estimate)
|
|
{
|
|
$results = ['success' => true, 'synced' => 0, 'errors' => []];
|
|
|
|
$estimate_items = $this->CI->estimates_model->get_estimate_items($perfex_estimate['id']);
|
|
|
|
foreach ($estimate_items as $item) {
|
|
if (!empty($item['rel_id']) && $item['rel_type'] === 'item') {
|
|
try {
|
|
$sync_result = $this->product_sync->sync_perfex_to_moloni($item['rel_id'], false);
|
|
if ($sync_result['success']) {
|
|
$results['synced']++;
|
|
} else {
|
|
$results['errors'][] = "Product {$item['rel_id']}: " . $sync_result['message'];
|
|
}
|
|
} catch (\Exception $e) {
|
|
$results['errors'][] = "Product {$item['rel_id']}: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($results['errors'])) {
|
|
$results['success'] = false;
|
|
$results['message'] = "Some products failed to sync: " . implode(', ', array_slice($results['errors'], 0, 3));
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Create or update estimate in Moloni
|
|
*
|
|
* @param array $moloni_data
|
|
* @param object $mapping
|
|
* @return array
|
|
*/
|
|
protected function create_or_update_moloni_estimate($moloni_data, $mapping = null)
|
|
{
|
|
if ($mapping && $mapping->moloni_id) {
|
|
// Update existing estimate
|
|
$response = $this->api_client->update_estimate($mapping->moloni_id, $moloni_data);
|
|
|
|
if ($response['success']) {
|
|
return [
|
|
'success' => true,
|
|
'estimate_id' => $mapping->moloni_id,
|
|
'action' => 'update',
|
|
'data' => $response['data']
|
|
];
|
|
}
|
|
}
|
|
|
|
// Create new estimate or fallback to create if update failed
|
|
$response = $this->api_client->create_estimate($moloni_data);
|
|
|
|
if ($response['success']) {
|
|
return [
|
|
'success' => true,
|
|
'estimate_id' => $response['data']['document_id'],
|
|
'action' => 'create',
|
|
'data' => $response['data']
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $response['message'] ?? 'Unknown error creating/updating estimate in Moloni'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create or update estimate in Perfex
|
|
*
|
|
* @param array $perfex_data
|
|
* @param object $mapping
|
|
* @return array
|
|
*/
|
|
protected function create_or_update_perfex_estimate($perfex_data, $mapping = null)
|
|
{
|
|
if ($mapping && $mapping->perfex_id) {
|
|
// Update existing estimate
|
|
$result = $this->CI->estimates_model->update($perfex_data, $mapping->perfex_id);
|
|
|
|
if ($result) {
|
|
return [
|
|
'success' => true,
|
|
'estimate_id' => $mapping->perfex_id,
|
|
'action' => 'update',
|
|
'data' => $perfex_data
|
|
];
|
|
}
|
|
}
|
|
|
|
// Create new estimate or fallback to create if update failed
|
|
$estimate_id = $this->CI->estimates_model->add($perfex_data);
|
|
|
|
if ($estimate_id) {
|
|
return [
|
|
'success' => true,
|
|
'estimate_id' => $estimate_id,
|
|
'action' => 'create',
|
|
'data' => $perfex_data
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to create/update estimate in Perfex CRM'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get Perfex estimate data
|
|
*
|
|
* @param int $estimate_id
|
|
* @return array|null
|
|
*/
|
|
protected function get_perfex_estimate($estimate_id)
|
|
{
|
|
$estimate = $this->CI->estimates_model->get($estimate_id);
|
|
return $estimate ? (array)$estimate : null;
|
|
}
|
|
|
|
/**
|
|
* Convert Perfex status to Moloni status
|
|
*
|
|
* @param int $perfex_status
|
|
* @return string
|
|
*/
|
|
protected function convert_perfex_status_to_moloni($perfex_status)
|
|
{
|
|
$status_mapping = [
|
|
self::STATUS_DRAFT => 'draft',
|
|
self::STATUS_SENT => 'sent',
|
|
self::STATUS_DECLINED => 'declined',
|
|
self::STATUS_ACCEPTED => 'accepted',
|
|
self::STATUS_EXPIRED => 'expired'
|
|
];
|
|
|
|
return $status_mapping[$perfex_status] ?? 'draft';
|
|
}
|
|
|
|
/**
|
|
* Convert Moloni status to Perfex status
|
|
*
|
|
* @param string $moloni_status
|
|
* @return int
|
|
*/
|
|
protected function convert_moloni_status_to_perfex($moloni_status)
|
|
{
|
|
$status_mapping = [
|
|
'draft' => self::STATUS_DRAFT,
|
|
'sent' => self::STATUS_SENT,
|
|
'declined' => self::STATUS_DECLINED,
|
|
'accepted' => self::STATUS_ACCEPTED,
|
|
'expired' => self::STATUS_EXPIRED
|
|
];
|
|
|
|
return $status_mapping[$moloni_status] ?? self::STATUS_DRAFT;
|
|
}
|
|
|
|
/**
|
|
* Calculate data hash for change detection
|
|
*
|
|
* @param array $data
|
|
* @return string
|
|
*/
|
|
protected function calculate_data_hash($data)
|
|
{
|
|
ksort($data);
|
|
return md5(serialize($data));
|
|
}
|
|
|
|
/**
|
|
* Handle sync error
|
|
*
|
|
* @param \Exception $e
|
|
* @param array $context
|
|
* @return array
|
|
*/
|
|
protected function handle_sync_error($e, $context)
|
|
{
|
|
$execution_time = $context['execution_time'];
|
|
|
|
// Update mapping with error if exists
|
|
if (isset($context['mapping']) && $context['mapping']) {
|
|
$this->entity_mapping->update_mapping_status(
|
|
$context['mapping']->id,
|
|
EntityMappingService::STATUS_ERROR,
|
|
$e->getMessage()
|
|
);
|
|
}
|
|
|
|
// Log error
|
|
$this->error_handler->log_error('sync', 'ESTIMATE_SYNC_FAILED', $e->getMessage(), $context);
|
|
|
|
// Log sync activity
|
|
$this->log_sync_activity([
|
|
'entity_type' => $context['entity_type'],
|
|
'entity_id' => $context['entity_id'],
|
|
'action' => 'sync',
|
|
'direction' => $context['direction'],
|
|
'status' => 'error',
|
|
'error_message' => $e->getMessage(),
|
|
'processing_time' => $execution_time
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
'execution_time' => $execution_time,
|
|
'error_code' => $e->getCode()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Log sync activity
|
|
*
|
|
* @param array $data
|
|
*/
|
|
protected function log_sync_activity($data)
|
|
{
|
|
$this->model->log_sync_activity($data);
|
|
}
|
|
|
|
// Additional helper methods for specific estimate functionality...
|
|
|
|
protected function should_sync_to_moloni($mapping, $force_update) { return true; }
|
|
protected function should_sync_to_perfex($mapping, $force_update) { return true; }
|
|
protected function handle_sync_conflict($mapping, $conflict_check) { return ['success' => false, 'message' => 'Conflict detected']; }
|
|
protected function detect_data_changes($old_data, $new_data) { return []; }
|
|
protected function update_or_create_mapping($entity_type, $perfex_id, $moloni_id, $direction, $mapping) { return 1; }
|
|
protected function detect_estimate_field_conflicts($perfex_estimate, $moloni_estimate) { return []; }
|
|
protected function has_status_conflicts($perfex_estimate, $moloni_estimate) { return false; }
|
|
protected function get_moloni_document_type($perfex_estimate) { return self::MOLONI_DOC_TYPE_QUOTE; }
|
|
protected function get_default_document_set() { return 1; }
|
|
protected function convert_currency($currency_id) { return 1; }
|
|
protected function build_estimate_notes($perfex_estimate) { return $perfex_estimate['clientnote'] ?? ''; }
|
|
protected function get_tax_exemption_reason($perfex_estimate) { return ''; }
|
|
protected function clean_moloni_estimate_data($data) { return $data; }
|
|
protected function clean_perfex_estimate_data($data) { return $data; }
|
|
protected function convert_moloni_currency_to_perfex($currency_id) { return 1; }
|
|
protected function get_item_tax_data($item) { return []; }
|
|
protected function sync_moloni_estimate_items($moloni_estimate, $perfex_estimate_id) { return true; }
|
|
protected function get_perfex_modification_time($estimate_id) { return time(); }
|
|
protected function get_moloni_modification_time($estimate_id) { return time(); }
|
|
} |