Files
desk-moloni/modules/desk_moloni/libraries/EstimateSyncService.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

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(); }
}