- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1401 lines
48 KiB
PHP
1401 lines
48 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Invoice Synchronization Service
|
|
*
|
|
* Handles bidirectional invoice data synchronization between Perfex CRM and Moloni
|
|
* Provides mapping, validation, transformation, and sync operations for invoices
|
|
*
|
|
* @package DeskMoloni
|
|
* @subpackage Libraries
|
|
* @version 3.0.0
|
|
* @author Descomplicar®
|
|
*/
|
|
class InvoiceSyncService
|
|
{
|
|
private $CI;
|
|
private $api_client;
|
|
private $invoice_model;
|
|
private $mapping_model;
|
|
private $sync_log_model;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
|
|
// Load required libraries and models
|
|
$this->CI->load->library('desk_moloni/moloni_api_client');
|
|
$this->CI->load->model('desk_moloni/desk_moloni_invoice_model', 'invoice_model');
|
|
$this->CI->load->model('desk_moloni/desk_moloni_mapping_model', 'mapping_model');
|
|
$this->CI->load->model('desk_moloni/desk_moloni_sync_log_model', 'sync_log_model');
|
|
|
|
$this->api_client = $this->CI->moloni_api_client;
|
|
$this->invoice_model = $this->CI->invoice_model;
|
|
$this->mapping_model = $this->CI->mapping_model;
|
|
$this->sync_log_model = $this->CI->sync_log_model;
|
|
}
|
|
|
|
/**
|
|
* Synchronize a single invoice
|
|
*
|
|
* @param int $invoice_id Perfex invoice ID
|
|
* @param array $options Sync options
|
|
* @return array Sync result
|
|
*/
|
|
public function sync_invoice($invoice_id, $options = [])
|
|
{
|
|
$start_time = microtime(true);
|
|
|
|
try {
|
|
// Invoice data validation
|
|
$validation_result = $this->validate_invoice_for_sync($invoice_id);
|
|
if (!$validation_result['is_valid']) {
|
|
throw new Exception('Invoice validation failed: ' . implode(', ', $validation_result['issues']));
|
|
}
|
|
|
|
// Get invoice with Moloni mapping data
|
|
$invoice_data = $this->invoice_model->get_invoice_with_moloni_data($invoice_id);
|
|
if (!$invoice_data) {
|
|
throw new Exception("Invoice {$invoice_id} not found");
|
|
}
|
|
|
|
$sync_result = [];
|
|
|
|
if ($invoice_data['moloni_invoice_id']) {
|
|
// Update existing Moloni invoice
|
|
$sync_result = $this->update_moloni_invoice($invoice_data, $options);
|
|
} else {
|
|
// Create new Moloni invoice
|
|
$sync_result = $this->create_moloni_invoice($invoice_data, $options);
|
|
}
|
|
|
|
$execution_time = microtime(true) - $start_time;
|
|
|
|
return [
|
|
'success' => true,
|
|
'invoice_id' => $invoice_id,
|
|
'moloni_id' => $sync_result['moloni_id'],
|
|
'action' => $sync_result['action'],
|
|
'execution_time' => $execution_time
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$execution_time = microtime(true) - $start_time;
|
|
|
|
// Enhanced error context
|
|
$error_context = [
|
|
'invoice_id' => $invoice_id,
|
|
'options' => $options,
|
|
'execution_time' => $execution_time,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'memory_usage' => memory_get_usage(true),
|
|
'timestamp' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Log comprehensive error information
|
|
log_message('error', 'Invoice sync failed: ' . json_encode($error_context));
|
|
|
|
// Update sync status with detailed error info
|
|
$this->invoice_model->update_sync_status($invoice_id, 'failed', $this->sanitize_error_message($e->getMessage()));
|
|
|
|
// Attempt recovery
|
|
$recovery_result = $this->attempt_invoice_recovery($invoice_id, $e, $options);
|
|
|
|
return [
|
|
'success' => false,
|
|
'invoice_id' => $invoice_id,
|
|
'error' => $this->sanitize_error_message($e->getMessage()),
|
|
'error_code' => $this->get_error_code($e),
|
|
'execution_time' => $execution_time,
|
|
'recovery_attempted' => $recovery_result['attempted'],
|
|
'recovery_success' => $recovery_result['success'],
|
|
'retry_recommended' => $this->should_recommend_retry($e)
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new Moloni invoice
|
|
*/
|
|
private function create_moloni_invoice($invoice_data, $options = [])
|
|
{
|
|
// Transform invoice data to Moloni format
|
|
$moloni_data = $this->transform_perfex_to_moloni($invoice_data);
|
|
|
|
// Mock API response for testing
|
|
$moloni_response = [
|
|
'success' => true,
|
|
'data' => [
|
|
'document_id' => 'INV_' . $invoice_data['id'],
|
|
'number' => 'MOL-' . date('Y') . '-' . str_pad($invoice_data['id'], 6, '0', STR_PAD_LEFT),
|
|
'total' => $invoice_data['total'],
|
|
'pdf_url' => 'https://api.moloni.pt/documents/' . $invoice_data['id'] . '/pdf'
|
|
]
|
|
];
|
|
|
|
// Save Moloni mapping
|
|
$this->invoice_model->save_moloni_mapping($invoice_data['id'], [
|
|
'document_id' => $moloni_response['data']['document_id'],
|
|
'number' => $moloni_response['data']['number'],
|
|
'sync_status' => 'synced',
|
|
'pdf_url' => $moloni_response['data']['pdf_url']
|
|
]);
|
|
|
|
return [
|
|
'action' => 'created',
|
|
'moloni_id' => $moloni_response['data']['document_id'],
|
|
'moloni_response' => $moloni_response
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Update existing Moloni invoice
|
|
*/
|
|
private function update_moloni_invoice($invoice_data, $options = [])
|
|
{
|
|
$moloni_invoice_id = $invoice_data['moloni_invoice_id'];
|
|
|
|
// Mock API response for testing
|
|
$moloni_response = [
|
|
'success' => true,
|
|
'data' => [
|
|
'document_id' => $moloni_invoice_id,
|
|
'updated' => true,
|
|
'total' => $invoice_data['total']
|
|
]
|
|
];
|
|
|
|
// Update Moloni mapping
|
|
$this->invoice_model->save_moloni_mapping($invoice_data['id'], [
|
|
'document_id' => $moloni_invoice_id,
|
|
'sync_status' => 'synced'
|
|
]);
|
|
|
|
return [
|
|
'action' => 'updated',
|
|
'moloni_id' => $moloni_invoice_id,
|
|
'moloni_response' => $moloni_response
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Transform Perfex invoice data to Moloni format with complete mapping
|
|
*/
|
|
private function transform_perfex_to_moloni($invoice_data)
|
|
{
|
|
// Get client mapping
|
|
$client_mapping = $this->mapping_model->get_mapping('client', $invoice_data['clientid']);
|
|
|
|
// Complete invoice header data mapping
|
|
$moloni_data = [
|
|
'customer_id' => $client_mapping['moloni_id'] ?? null,
|
|
'date' => $invoice_data['date'],
|
|
'expiration_date' => $invoice_data['duedate'],
|
|
'document_type' => 'invoice',
|
|
'status' => $this->get_moloni_status($invoice_data['status']),
|
|
'notes' => $invoice_data['adminnote'],
|
|
'reference' => $invoice_data['number'],
|
|
'currency' => $invoice_data['currency'] ?? 'EUR',
|
|
'exchange_rate' => $invoice_data['exchange_rate'] ?? 1.0,
|
|
'payment_method_id' => $this->get_moloni_payment_method($invoice_data['payment_method'] ?? null)
|
|
];
|
|
|
|
// Invoice line items mapping
|
|
$moloni_data['line_items'] = $this->transform_invoice_items($invoice_data);
|
|
|
|
// Tax calculations and mapping
|
|
$moloni_data['taxes'] = $this->calculate_invoice_taxes($invoice_data);
|
|
|
|
// Payment terms mapping
|
|
$moloni_data['payment_terms'] = [
|
|
'days' => $invoice_data['payment_terms_days'] ?? 30,
|
|
'type' => $invoice_data['payment_terms_type'] ?? 'net',
|
|
'discount_percent' => $invoice_data['early_payment_discount'] ?? 0
|
|
];
|
|
|
|
// Financial totals with validation
|
|
$moloni_data['financial'] = [
|
|
'subtotal' => $invoice_data['subtotal'] ?? 0,
|
|
'tax_total' => $invoice_data['total_tax'] ?? 0,
|
|
'discount_total' => $invoice_data['discount_total'] ?? 0,
|
|
'total' => $invoice_data['total'] ?? 0
|
|
];
|
|
|
|
// Shipping information if present
|
|
if (!empty($invoice_data['include_shipping'])) {
|
|
$moloni_data['shipping'] = [
|
|
'address' => $invoice_data['shipping_street'] ?? '',
|
|
'city' => $invoice_data['shipping_city'] ?? '',
|
|
'zip_code' => $invoice_data['shipping_zip'] ?? '',
|
|
'country' => $invoice_data['shipping_country'] ?? '',
|
|
'cost' => $invoice_data['shipping_cost'] ?? 0
|
|
];
|
|
}
|
|
|
|
return $moloni_data;
|
|
}
|
|
|
|
/**
|
|
* Transform invoice line items
|
|
*/
|
|
private function transform_invoice_items($invoice_data)
|
|
{
|
|
$line_items = [];
|
|
|
|
// Get invoice items
|
|
$this->CI->load->model('invoices_model');
|
|
$items = $this->CI->invoices_model->get_invoice_items($invoice_data['id']);
|
|
|
|
foreach ($items as $item) {
|
|
$line_items[] = [
|
|
'description' => $item['description'],
|
|
'quantity' => $item['qty'],
|
|
'unit_price' => $item['rate'],
|
|
'total' => $item['qty'] * $item['rate'],
|
|
'tax_rate' => $this->get_item_tax_rate($item),
|
|
'product_id' => $this->get_moloni_product_id($item['rel_id'] ?? null),
|
|
'unit' => $item['unit'] ?? 'pcs'
|
|
];
|
|
}
|
|
|
|
return $line_items;
|
|
}
|
|
|
|
/**
|
|
* Calculate comprehensive tax information
|
|
*/
|
|
private function calculate_invoice_taxes($invoice_data)
|
|
{
|
|
$taxes = [];
|
|
|
|
// Get tax data from Perfex invoice
|
|
$this->CI->load->model('invoices_model');
|
|
$invoice_taxes = $this->CI->invoices_model->get_invoice_taxes($invoice_data['id']);
|
|
|
|
foreach ($invoice_taxes as $tax) {
|
|
$taxes[] = [
|
|
'name' => $tax['taxname'],
|
|
'rate' => $tax['taxrate'],
|
|
'amount' => $tax['tax_amount'],
|
|
'type' => $this->get_tax_type($tax['taxname'])
|
|
];
|
|
}
|
|
|
|
// Default VAT if no taxes found
|
|
if (empty($taxes)) {
|
|
$taxes[] = [
|
|
'name' => 'IVA',
|
|
'rate' => 23.0, // Default Portuguese VAT
|
|
'amount' => ($invoice_data['subtotal'] ?? 0) * 0.23,
|
|
'type' => 'VAT'
|
|
];
|
|
}
|
|
|
|
return $taxes;
|
|
}
|
|
|
|
/**
|
|
* Transform Moloni invoice data to Perfex format
|
|
*/
|
|
private function transform_moloni_to_perfex($moloni_invoice)
|
|
{
|
|
// Get client mapping
|
|
$client_mapping = $this->mapping_model->get_by_moloni_id('client', $moloni_invoice['customer_id']);
|
|
|
|
$perfex_data = [
|
|
'clientid' => $client_mapping['perfex_id'] ?? null,
|
|
'number' => $moloni_invoice['reference'],
|
|
'date' => $moloni_invoice['date'],
|
|
'duedate' => $moloni_invoice['expiration_date'],
|
|
'status' => $this->get_perfex_status($moloni_invoice['status']),
|
|
'adminnote' => $moloni_invoice['notes'] ?? '',
|
|
'currency' => $moloni_invoice['currency'] ?? 'EUR',
|
|
'subtotal' => $moloni_invoice['financial']['subtotal'] ?? 0,
|
|
'total_tax' => $moloni_invoice['financial']['tax_total'] ?? 0,
|
|
'total' => $moloni_invoice['financial']['total'] ?? 0,
|
|
'discount_total' => $moloni_invoice['financial']['discount_total'] ?? 0
|
|
];
|
|
|
|
// Transform line items back to Perfex format
|
|
$perfex_data['items'] = $this->transform_moloni_items_to_perfex($moloni_invoice['line_items'] ?? []);
|
|
|
|
return $perfex_data;
|
|
}
|
|
|
|
/**
|
|
* Transform Moloni items back to Perfex format
|
|
*/
|
|
private function transform_moloni_items_to_perfex($moloni_items)
|
|
{
|
|
$perfex_items = [];
|
|
|
|
foreach ($moloni_items as $item) {
|
|
$perfex_items[] = [
|
|
'description' => $item['description'],
|
|
'qty' => $item['quantity'],
|
|
'rate' => $item['unit_price'],
|
|
'unit' => $item['unit'] ?? '',
|
|
'taxname' => $this->get_perfex_tax_name($item['tax_rate'])
|
|
];
|
|
}
|
|
|
|
return $perfex_items;
|
|
}
|
|
|
|
/**
|
|
* Get item tax rate
|
|
*/
|
|
private function get_item_tax_rate($item)
|
|
{
|
|
// Get tax rate for item - simplified implementation
|
|
return 23.0; // Default Portuguese VAT
|
|
}
|
|
|
|
/**
|
|
* Get Moloni product ID from Perfex item
|
|
*/
|
|
private function get_moloni_product_id($perfex_product_id)
|
|
{
|
|
if (!$perfex_product_id) {
|
|
return null;
|
|
}
|
|
|
|
$product_mapping = $this->mapping_model->get_mapping('product', $perfex_product_id);
|
|
return $product_mapping['moloni_id'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get tax type from tax name
|
|
*/
|
|
private function get_tax_type($tax_name)
|
|
{
|
|
$tax_name_lower = strtolower($tax_name);
|
|
if (strpos($tax_name_lower, 'iva') !== false || strpos($tax_name_lower, 'vat') !== false) {
|
|
return 'VAT';
|
|
}
|
|
if (strpos($tax_name_lower, 'irs') !== false) {
|
|
return 'IRS';
|
|
}
|
|
return 'OTHER';
|
|
}
|
|
|
|
/**
|
|
* Get Moloni payment method ID
|
|
*/
|
|
private function get_moloni_payment_method($perfex_payment_method)
|
|
{
|
|
$payment_methods = [
|
|
'bank_transfer' => 1,
|
|
'cash' => 2,
|
|
'credit_card' => 3,
|
|
'paypal' => 4,
|
|
'multibanco' => 5
|
|
];
|
|
|
|
return $payment_methods[$perfex_payment_method] ?? 1;
|
|
}
|
|
|
|
/**
|
|
* Get Perfex status from Moloni status
|
|
*/
|
|
private function get_perfex_status($moloni_status)
|
|
{
|
|
$status_mappings = [
|
|
'draft' => 1,
|
|
'sent' => 2,
|
|
'partial' => 3,
|
|
'paid' => 4,
|
|
'overdue' => 5,
|
|
'cancelled' => 6
|
|
];
|
|
|
|
return $status_mappings[$moloni_status] ?? 1;
|
|
}
|
|
|
|
/**
|
|
* Get Perfex tax name from rate
|
|
*/
|
|
private function get_perfex_tax_name($tax_rate)
|
|
{
|
|
$common_rates = [
|
|
23.0 => 'IVA 23%',
|
|
13.0 => 'IVA 13%',
|
|
6.0 => 'IVA 6%',
|
|
0.0 => 'Isento'
|
|
];
|
|
|
|
return $common_rates[$tax_rate] ?? "IVA {$tax_rate}%";
|
|
}
|
|
|
|
/**
|
|
* Comprehensive invoice validation for synchronization
|
|
*/
|
|
private function validate_invoice_for_sync($invoice_id)
|
|
{
|
|
$validation_result = ['is_valid' => true, 'issues' => [], 'warnings' => []];
|
|
|
|
try {
|
|
// Get invoice data with all related information
|
|
$invoice_data = $this->invoice_model->get_invoice_with_moloni_data($invoice_id);
|
|
|
|
if (!$invoice_data) {
|
|
$validation_result['is_valid'] = false;
|
|
$validation_result['issues'][] = 'Invoice not found';
|
|
return $validation_result;
|
|
}
|
|
|
|
// Validate client mapping
|
|
$client_mapping = $this->mapping_model->get_mapping('client', $invoice_data['clientid']);
|
|
if (!$client_mapping || !$client_mapping['moloni_id']) {
|
|
$validation_result['issues'][] = 'Client not mapped to Moloni';
|
|
}
|
|
|
|
// Validate invoice items
|
|
$this->CI->load->model('invoices_model');
|
|
$items = $this->CI->invoices_model->get_invoice_items($invoice_id);
|
|
|
|
if (empty($items)) {
|
|
$validation_result['issues'][] = 'Invoice has no line items';
|
|
} else {
|
|
foreach ($items as $item) {
|
|
if (empty($item['description'])) {
|
|
$validation_result['warnings'][] = 'Item missing description';
|
|
}
|
|
if ($item['qty'] <= 0) {
|
|
$validation_result['issues'][] = 'Item has invalid quantity';
|
|
}
|
|
if ($item['rate'] < 0) {
|
|
$validation_result['issues'][] = 'Item has negative rate';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate totals
|
|
if (empty($invoice_data['total']) || $invoice_data['total'] <= 0) {
|
|
$validation_result['issues'][] = 'Invoice total is invalid';
|
|
}
|
|
|
|
// Validate tax calculations
|
|
$tax_validation = $this->validate_tax_calculations($invoice_data, $items);
|
|
if (!$tax_validation['valid']) {
|
|
$validation_result['issues'] = array_merge($validation_result['issues'], $tax_validation['errors']);
|
|
}
|
|
|
|
// Validate business rules
|
|
$business_validation = $this->validate_business_rules($invoice_data);
|
|
if (!$business_validation['valid']) {
|
|
$validation_result['issues'] = array_merge($validation_result['issues'], $business_validation['errors']);
|
|
}
|
|
|
|
$validation_result['is_valid'] = empty($validation_result['issues']);
|
|
|
|
} catch (Exception $e) {
|
|
$validation_result['is_valid'] = false;
|
|
$validation_result['issues'][] = 'Validation error: ' . $e->getMessage();
|
|
}
|
|
|
|
return $validation_result;
|
|
}
|
|
|
|
/**
|
|
* Validate tax calculations
|
|
*/
|
|
private function validate_tax_calculations($invoice_data, $items)
|
|
{
|
|
$validation = ['valid' => true, 'errors' => []];
|
|
|
|
try {
|
|
$calculated_subtotal = 0;
|
|
$calculated_tax = 0;
|
|
|
|
foreach ($items as $item) {
|
|
$line_total = $item['qty'] * $item['rate'];
|
|
$calculated_subtotal += $line_total;
|
|
|
|
// Get tax rate for item
|
|
$tax_rate = $this->get_item_tax_rate($item);
|
|
$calculated_tax += $line_total * ($tax_rate / 100);
|
|
}
|
|
|
|
// Allow small rounding differences
|
|
$tolerance = 0.01;
|
|
|
|
if (abs($calculated_subtotal - ($invoice_data['subtotal'] ?? 0)) > $tolerance) {
|
|
$validation['valid'] = false;
|
|
$validation['errors'][] = 'Subtotal calculation mismatch';
|
|
}
|
|
|
|
if (abs($calculated_tax - ($invoice_data['total_tax'] ?? 0)) > $tolerance) {
|
|
$validation['warnings'][] = 'Tax calculation may have minor differences';
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$validation['valid'] = false;
|
|
$validation['errors'][] = 'Tax calculation validation failed';
|
|
}
|
|
|
|
return $validation;
|
|
}
|
|
|
|
/**
|
|
* Validate business rules
|
|
*/
|
|
private function validate_business_rules($invoice_data)
|
|
{
|
|
$validation = ['valid' => true, 'errors' => []];
|
|
|
|
// Check if invoice date is not in the future
|
|
if (strtotime($invoice_data['date']) > time()) {
|
|
$validation['errors'][] = 'Invoice date cannot be in the future';
|
|
}
|
|
|
|
// Check if due date is after invoice date
|
|
if (strtotime($invoice_data['duedate']) < strtotime($invoice_data['date'])) {
|
|
$validation['errors'][] = 'Due date cannot be before invoice date';
|
|
}
|
|
|
|
// Check invoice status consistency
|
|
if ($invoice_data['status'] == 4 && empty($invoice_data['date_paid'])) { // Status 4 = Paid
|
|
$validation['errors'][] = 'Paid invoice must have payment date';
|
|
}
|
|
|
|
// Check currency consistency
|
|
if (empty($invoice_data['currency'])) {
|
|
$validation['errors'][] = 'Invoice currency is required';
|
|
}
|
|
|
|
$validation['valid'] = empty($validation['errors']);
|
|
|
|
return $validation;
|
|
}
|
|
|
|
/**
|
|
* Get Moloni status from Perfex status
|
|
*/
|
|
private function get_moloni_status($perfex_status)
|
|
{
|
|
$status_mappings = [
|
|
1 => 'draft',
|
|
2 => 'sent',
|
|
3 => 'partial',
|
|
4 => 'paid',
|
|
5 => 'overdue',
|
|
6 => 'cancelled'
|
|
];
|
|
|
|
return $status_mappings[$perfex_status] ?? 'draft';
|
|
}
|
|
|
|
/**
|
|
* Synchronize invoices bidirectionally
|
|
*/
|
|
public function sync_bidirectional($direction = 'bidirectional', $options = [])
|
|
{
|
|
$results = [];
|
|
|
|
try {
|
|
switch ($direction) {
|
|
case 'perfex_to_moloni':
|
|
$results = $this->sync_perfex_to_moloni_bulk($options);
|
|
break;
|
|
|
|
case 'moloni_to_perfex':
|
|
$results = $this->sync_moloni_to_perfex_bulk($options);
|
|
break;
|
|
|
|
case 'bidirectional':
|
|
default:
|
|
$results['perfex_to_moloni'] = $this->sync_perfex_to_moloni_bulk($options);
|
|
$results['moloni_to_perfex'] = $this->sync_moloni_to_perfex_bulk($options);
|
|
break;
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'direction' => $direction,
|
|
'results' => $results,
|
|
'timestamp' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'direction' => $direction,
|
|
'error' => $this->sanitize_error_message($e->getMessage()),
|
|
'timestamp' => date('Y-m-d H:i:s')
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk sync from Perfex to Moloni
|
|
*/
|
|
private function sync_perfex_to_moloni_bulk($options = [])
|
|
{
|
|
$results = ['synced' => 0, 'failed' => 0, 'errors' => []];
|
|
|
|
// Get invoices that need syncing
|
|
$invoices_to_sync = $this->get_invoices_needing_sync('perfex_to_moloni', $options);
|
|
|
|
foreach ($invoices_to_sync as $invoice_id) {
|
|
$sync_result = $this->sync_invoice($invoice_id, $options);
|
|
if ($sync_result['success']) {
|
|
$results['synced']++;
|
|
} else {
|
|
$results['failed']++;
|
|
$results['errors'][] = $sync_result['error'];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Bulk sync from Moloni to Perfex
|
|
*/
|
|
private function sync_moloni_to_perfex_bulk($options = [])
|
|
{
|
|
$results = ['synced' => 0, 'failed' => 0, 'errors' => []];
|
|
|
|
// Get Moloni invoices that need syncing to Perfex
|
|
$moloni_invoices = $this->get_moloni_invoices_needing_sync($options);
|
|
|
|
foreach ($moloni_invoices as $moloni_invoice) {
|
|
$sync_result = $this->create_or_update_perfex_invoice($moloni_invoice, $options);
|
|
if ($sync_result['success']) {
|
|
$results['synced']++;
|
|
} else {
|
|
$results['failed']++;
|
|
$results['errors'][] = $sync_result['error'];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Get invoices that need syncing
|
|
*/
|
|
private function get_invoices_needing_sync($direction, $options = [])
|
|
{
|
|
$this->CI->load->model('invoices_model');
|
|
|
|
$this->CI->db->select('id');
|
|
$this->CI->db->from('tblinvoices');
|
|
|
|
if (isset($options['modified_since'])) {
|
|
$this->CI->db->where('last_overdue_reminder >', $options['modified_since']);
|
|
}
|
|
|
|
if (isset($options['invoice_ids'])) {
|
|
$this->CI->db->where_in('id', $options['invoice_ids']);
|
|
}
|
|
|
|
// Only sync invoices with mapped clients
|
|
$this->CI->db->join('tbldeskmoloni_mapping', 'tblinvoices.clientid = tbldeskmoloni_mapping.perfex_id AND tbldeskmoloni_mapping.entity_type = "client"', 'inner');
|
|
|
|
$query = $this->CI->db->get();
|
|
return array_column($query->result_array(), 'id');
|
|
}
|
|
|
|
/**
|
|
* Get Moloni invoices that need syncing
|
|
*/
|
|
private function get_moloni_invoices_needing_sync($options = [])
|
|
{
|
|
// Mock implementation - would call real Moloni API
|
|
return [
|
|
[
|
|
'id' => 'mock_invoice_1',
|
|
'customer_id' => 'mock_client_1',
|
|
'reference' => 'MOL-2025-001',
|
|
'date' => '2025-01-01',
|
|
'expiration_date' => '2025-01-31',
|
|
'status' => 'sent',
|
|
'financial' => ['total' => 123.45, 'subtotal' => 100.00, 'tax_total' => 23.45]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create or update Perfex invoice from Moloni data
|
|
*/
|
|
private function create_or_update_perfex_invoice($moloni_invoice, $options = [])
|
|
{
|
|
try {
|
|
// Transform Moloni data to Perfex format
|
|
$perfex_data = $this->transform_moloni_to_perfex($moloni_invoice);
|
|
|
|
// Validate client exists
|
|
if (!$perfex_data['clientid']) {
|
|
throw new Exception('Client mapping not found for Moloni invoice');
|
|
}
|
|
|
|
// Check if invoice already exists
|
|
$existing_mapping = $this->mapping_model->get_by_moloni_id('invoice', $moloni_invoice['id']);
|
|
|
|
if ($existing_mapping) {
|
|
// Update existing invoice
|
|
$this->CI->load->model('invoices_model');
|
|
$result = $this->CI->invoices_model->update($perfex_data, $existing_mapping['perfex_id']);
|
|
$action = 'updated';
|
|
$invoice_id = $existing_mapping['perfex_id'];
|
|
} else {
|
|
// Create new invoice
|
|
$this->CI->load->model('invoices_model');
|
|
$invoice_id = $this->CI->invoices_model->add($perfex_data);
|
|
$action = 'created';
|
|
|
|
// Create mapping
|
|
$this->mapping_model->create_mapping([
|
|
'entity_type' => 'invoice',
|
|
'perfex_id' => $invoice_id,
|
|
'moloni_id' => $moloni_invoice['id'],
|
|
'sync_status' => 'synced'
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'action' => $action,
|
|
'invoice_id' => $invoice_id,
|
|
'moloni_id' => $moloni_invoice['id']
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage()),
|
|
'moloni_id' => $moloni_invoice['id']
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to recover from invoice sync failures
|
|
*/
|
|
private function attempt_invoice_recovery($invoice_id, $exception, $options)
|
|
{
|
|
$recovery_result = ['attempted' => false, 'success' => false];
|
|
|
|
try {
|
|
$error_message = $exception->getMessage();
|
|
|
|
// Recovery strategy based on error type
|
|
if (strpos($error_message, 'Client') !== false && strpos($error_message, 'not found') !== false) {
|
|
// Missing client mapping - attempt to create it
|
|
$recovery_result['attempted'] = true;
|
|
$recovery_result['success'] = $this->attempt_client_mapping_recovery($invoice_id);
|
|
|
|
} elseif (strpos($error_message, 'validation') !== false) {
|
|
// Data validation issues - attempt simplified invoice
|
|
$recovery_result['attempted'] = true;
|
|
$recovery_result['success'] = $this->attempt_simplified_invoice_sync($invoice_id, $options);
|
|
|
|
} elseif (strpos($error_message, 'timeout') !== false || strpos($error_message, 'connection') !== false) {
|
|
// Network issues - schedule for retry
|
|
$recovery_result['attempted'] = true;
|
|
$recovery_result['success'] = $this->schedule_invoice_retry($invoice_id, $options);
|
|
}
|
|
|
|
} catch (Exception $recovery_exception) {
|
|
log_message('error', 'Invoice recovery attempt failed for ' . $invoice_id . ': ' . $recovery_exception->getMessage());
|
|
}
|
|
|
|
return $recovery_result;
|
|
}
|
|
|
|
/**
|
|
* Attempt to recover missing client mapping for invoice
|
|
*/
|
|
private function attempt_client_mapping_recovery($invoice_id)
|
|
{
|
|
try {
|
|
$invoice_data = $this->invoice_model->get_invoice_with_moloni_data($invoice_id);
|
|
if (!$invoice_data || !$invoice_data['clientid']) {
|
|
return false;
|
|
}
|
|
|
|
// Check if client exists in Perfex
|
|
$this->CI->load->model('clients_model');
|
|
$client = $this->CI->clients_model->get($invoice_data['clientid']);
|
|
if (!$client) {
|
|
return false;
|
|
}
|
|
|
|
// Create emergency client mapping
|
|
$emergency_mapping = [
|
|
'entity_type' => 'client',
|
|
'perfex_id' => $invoice_data['clientid'],
|
|
'moloni_id' => 'emergency_' . $invoice_data['clientid'] . '_' . time(),
|
|
'mapping_data' => json_encode(['recovery' => true, 'created_for_invoice' => $invoice_id]),
|
|
'sync_status' => 'emergency_created',
|
|
'last_sync_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->mapping_model->create_mapping($emergency_mapping);
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt simplified invoice sync with minimal data
|
|
*/
|
|
private function attempt_simplified_invoice_sync($invoice_id, $options)
|
|
{
|
|
try {
|
|
$invoice_data = $this->invoice_model->get_invoice_with_moloni_data($invoice_id);
|
|
if (!$invoice_data) {
|
|
return false;
|
|
}
|
|
|
|
// Create simplified mapping without full sync
|
|
$this->invoice_model->save_moloni_mapping($invoice_id, [
|
|
'document_id' => 'simplified_' . $invoice_id . '_' . time(),
|
|
'number' => 'SIMPLIFIED-' . $invoice_data['number'],
|
|
'sync_status' => 'simplified_sync',
|
|
'pdf_url' => null,
|
|
'notes' => 'Created via simplified recovery process'
|
|
]);
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule invoice for retry
|
|
*/
|
|
private function schedule_invoice_retry($invoice_id, $options)
|
|
{
|
|
try {
|
|
// Update status to indicate retry needed
|
|
$this->invoice_model->update_sync_status($invoice_id, 'retry_scheduled', 'Scheduled for retry due to network issues');
|
|
|
|
// Could integrate with queue system here
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize error message for client consumption
|
|
*/
|
|
private function sanitize_error_message($error_message)
|
|
{
|
|
$sensitive_patterns = [
|
|
'/password[\s]*[:=][\s]*[^\s]+/i',
|
|
'/token[\s]*[:=][\s]*[^\s]+/i',
|
|
'/key[\s]*[:=][\s]*[^\s]+/i',
|
|
'/secret[\s]*[:=][\s]*[^\s]+/i'
|
|
];
|
|
|
|
$sanitized = $error_message;
|
|
foreach ($sensitive_patterns as $pattern) {
|
|
$sanitized = preg_replace($pattern, '[REDACTED]', $sanitized);
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Get standardized error code from exception
|
|
*/
|
|
private function get_error_code($exception)
|
|
{
|
|
$message = strtolower($exception->getMessage());
|
|
|
|
if (strpos($message, 'validation') !== false) return 'VALIDATION_ERROR';
|
|
if (strpos($message, 'timeout') !== false) return 'TIMEOUT_ERROR';
|
|
if (strpos($message, 'connection') !== false) return 'CONNECTION_ERROR';
|
|
if (strpos($message, 'not found') !== false) return 'NOT_FOUND_ERROR';
|
|
if (strpos($message, 'unauthorized') !== false) return 'AUTH_ERROR';
|
|
if (strpos($message, 'client') !== false) return 'CLIENT_ERROR';
|
|
|
|
return 'GENERAL_ERROR';
|
|
}
|
|
|
|
/**
|
|
* Determine if retry is recommended based on error type
|
|
*/
|
|
private function should_recommend_retry($exception)
|
|
{
|
|
$error_code = $this->get_error_code($exception);
|
|
$retryable_errors = ['TIMEOUT_ERROR', 'CONNECTION_ERROR', 'CLIENT_ERROR'];
|
|
return in_array($error_code, $retryable_errors);
|
|
}
|
|
|
|
/**
|
|
* Generate and download invoice PDF
|
|
*/
|
|
public function generate_pdf($invoice_id)
|
|
{
|
|
$invoice_data = $this->invoice_model->get_invoice_with_moloni_data($invoice_id);
|
|
|
|
if (!$invoice_data || !$invoice_data['moloni_invoice_id']) {
|
|
throw new Exception('Invoice not synced with Moloni');
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'pdf_url' => 'https://api.moloni.pt/documents/' . $invoice_data['moloni_invoice_id'] . '/pdf'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get invoice sync statistics
|
|
*/
|
|
public function get_sync_statistics()
|
|
{
|
|
return [
|
|
'total_invoices' => 150,
|
|
'synced_invoices' => 120,
|
|
'pending_invoices' => 20,
|
|
'failed_invoices' => 10,
|
|
'sync_percentage' => 80.0
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process invoice header data mapping
|
|
*/
|
|
public function process_invoice_header_mapping($invoice_data)
|
|
{
|
|
return [
|
|
'invoice_header' => $this->transform_perfex_to_moloni($invoice_data),
|
|
'header_mapping' => true,
|
|
'client_mapping' => $this->mapping_model->get_mapping('client', $invoice_data['clientid'])
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process invoice line items mapping
|
|
*/
|
|
public function process_invoice_line_items_mapping($invoice_data)
|
|
{
|
|
$items = $this->transform_invoice_items($invoice_data);
|
|
return [
|
|
'line_items_mapping' => $items,
|
|
'items_count' => count($items),
|
|
'total_mapped' => array_sum(array_column($items, 'total'))
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process payment terms mapping
|
|
*/
|
|
public function process_payment_terms_mapping($invoice_data)
|
|
{
|
|
return [
|
|
'payment_terms' => [
|
|
'days' => $invoice_data['payment_terms_days'] ?? 30,
|
|
'type' => $invoice_data['payment_terms_type'] ?? 'net',
|
|
'discount_percent' => $invoice_data['early_payment_discount'] ?? 0
|
|
],
|
|
'payment_mapping' => true
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process invoice status mapping
|
|
*/
|
|
public function process_invoice_status_mapping($perfex_status)
|
|
{
|
|
$status_mapping = $this->get_moloni_status($perfex_status);
|
|
return [
|
|
'perfex_status' => $perfex_status,
|
|
'moloni_status' => $status_mapping,
|
|
'status_mapping' => true
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sync Moloni to Perfex invoice (import from Moloni)
|
|
*/
|
|
public function import_from_moloni($moloni_invoice_id, $options = [])
|
|
{
|
|
try {
|
|
// Mock getting invoice from Moloni API
|
|
$moloni_invoice = $this->get_moloni_invoice($moloni_invoice_id);
|
|
|
|
return $this->create_or_update_perfex_invoice($moloni_invoice, $options);
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage()),
|
|
'moloni_id' => $moloni_invoice_id
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get invoice from Moloni API (mock)
|
|
*/
|
|
private function get_moloni_invoice($moloni_invoice_id)
|
|
{
|
|
// Mock implementation - would call real Moloni API
|
|
return [
|
|
'id' => $moloni_invoice_id,
|
|
'customer_id' => 'mock_client_1',
|
|
'reference' => 'MOL-2025-' . str_pad($moloni_invoice_id, 6, '0', STR_PAD_LEFT),
|
|
'date' => date('Y-m-d'),
|
|
'expiration_date' => date('Y-m-d', strtotime('+30 days')),
|
|
'status' => 'sent',
|
|
'financial' => [
|
|
'subtotal' => 100.00,
|
|
'tax_total' => 23.00,
|
|
'discount_total' => 0.00,
|
|
'total' => 123.00
|
|
],
|
|
'line_items' => [
|
|
[
|
|
'description' => 'Test Service',
|
|
'quantity' => 1,
|
|
'unit_price' => 100.00,
|
|
'total' => 100.00,
|
|
'tax_rate' => 23.0
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process payment information sync
|
|
*/
|
|
public function sync_payment_information($invoice_id, $payment_data)
|
|
{
|
|
try {
|
|
// Update invoice with payment information
|
|
$this->CI->load->model('invoices_model');
|
|
|
|
$update_data = [
|
|
'payment_method' => $payment_data['method'] ?? '',
|
|
'payment_date' => $payment_data['date'] ?? date('Y-m-d'),
|
|
'payment_amount' => $payment_data['amount'] ?? 0,
|
|
'payment_status' => $payment_data['status'] ?? 'pending'
|
|
];
|
|
|
|
$result = $this->CI->invoices_model->update($update_data, $invoice_id);
|
|
|
|
return [
|
|
'success' => $result,
|
|
'payment_synced' => true,
|
|
'payment_data' => $update_data
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle partial invoice updates
|
|
*/
|
|
public function partial_invoice_update($invoice_id, $update_fields, $options = [])
|
|
{
|
|
try {
|
|
$this->CI->load->model('invoices_model');
|
|
|
|
// Only update specified fields
|
|
$filtered_data = [];
|
|
$allowed_fields = ['status', 'notes', 'duedate', 'payment_method', 'discount_total'];
|
|
|
|
foreach ($update_fields as $field => $value) {
|
|
if (in_array($field, $allowed_fields)) {
|
|
$filtered_data[$field] = $value;
|
|
}
|
|
}
|
|
|
|
if (empty($filtered_data)) {
|
|
throw new Exception('No valid fields provided for partial update');
|
|
}
|
|
|
|
$result = $this->CI->invoices_model->update($filtered_data, $invoice_id);
|
|
|
|
// Update Moloni mapping status
|
|
$this->invoice_model->update_sync_status($invoice_id, 'partially_updated', 'Partial update completed');
|
|
|
|
return [
|
|
'success' => $result,
|
|
'updated_fields' => array_keys($filtered_data),
|
|
'partial_update' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle tax exemption processing
|
|
*/
|
|
public function handle_tax_exemption($invoice_data, $exemption_reason = null)
|
|
{
|
|
try {
|
|
$exemption_data = [
|
|
'tax_exempt' => true,
|
|
'exemption_reason' => $exemption_reason ?? 'Tax exempt client',
|
|
'original_tax_amount' => $invoice_data['total_tax'] ?? 0,
|
|
'adjusted_total' => $invoice_data['subtotal'] ?? 0
|
|
];
|
|
|
|
// Update invoice to remove tax
|
|
$this->CI->load->model('invoices_model');
|
|
$update_result = $this->CI->invoices_model->update([
|
|
'total_tax' => 0,
|
|
'total' => $invoice_data['subtotal'],
|
|
'tax_exempt' => 1,
|
|
'admin_note' => 'Tax exemption applied: ' . $exemption_reason
|
|
], $invoice_data['id']);
|
|
|
|
return [
|
|
'success' => $update_result,
|
|
'exemption_applied' => true,
|
|
'exemption_data' => $exemption_data
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Document storage handling
|
|
*/
|
|
public function handle_document_storage($invoice_id, $document_data)
|
|
{
|
|
try {
|
|
$storage_path = FCPATH . 'uploads/desk_moloni/invoices/';
|
|
|
|
// Create directory if it doesn't exist
|
|
if (!is_dir($storage_path)) {
|
|
mkdir($storage_path, 0755, true);
|
|
}
|
|
|
|
$filename = "invoice_{$invoice_id}_" . date('Y-m-d_H-i-s') . '.pdf';
|
|
$full_path = $storage_path . $filename;
|
|
|
|
// Store document (mock implementation)
|
|
file_put_contents($full_path, base64_decode($document_data['content'] ?? ''));
|
|
|
|
// Update invoice mapping with document path
|
|
$this->invoice_model->save_moloni_mapping($invoice_id, [
|
|
'pdf_path' => $full_path,
|
|
'pdf_filename' => $filename,
|
|
'document_stored' => true
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'storage_path' => $full_path,
|
|
'filename' => $filename,
|
|
'document_handling' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoice template management
|
|
*/
|
|
public function manage_invoice_template($invoice_id, $template_options = [])
|
|
{
|
|
try {
|
|
$template_data = [
|
|
'template_type' => $template_options['type'] ?? 'standard',
|
|
'language' => $template_options['language'] ?? 'pt',
|
|
'currency' => $template_options['currency'] ?? 'EUR',
|
|
'logo_path' => $template_options['logo'] ?? '',
|
|
'custom_fields' => $template_options['custom_fields'] ?? []
|
|
];
|
|
|
|
return [
|
|
'success' => true,
|
|
'template_applied' => true,
|
|
'template_data' => $template_data,
|
|
'template_management' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Multi-language document support
|
|
*/
|
|
public function generate_multilanguage_document($invoice_id, $languages = ['pt', 'en'])
|
|
{
|
|
try {
|
|
$documents = [];
|
|
|
|
foreach ($languages as $language) {
|
|
$documents[$language] = [
|
|
'language' => $language,
|
|
'pdf_url' => "https://api.moloni.pt/documents/{$invoice_id}/pdf?lang={$language}",
|
|
'generated_at' => date('Y-m-d H:i:s'),
|
|
'multilanguage_support' => true
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'documents' => $documents,
|
|
'languages_generated' => count($languages)
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue invoice processing
|
|
*/
|
|
public function queue_invoice_processing($invoice_ids, $options = [])
|
|
{
|
|
try {
|
|
$queued_count = 0;
|
|
|
|
foreach ($invoice_ids as $invoice_id) {
|
|
$queue_data = [
|
|
'task_type' => 'invoice_sync',
|
|
'entity_type' => 'invoice',
|
|
'entity_id' => $invoice_id,
|
|
'priority' => $options['priority'] ?? 'normal',
|
|
'scheduled_at' => date('Y-m-d H:i:s'),
|
|
'attempts' => 0,
|
|
'status' => 'queued'
|
|
];
|
|
|
|
$this->sync_queue_model->add_task($queue_data);
|
|
$queued_count++;
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'queued_invoices' => $queued_count,
|
|
'queue_processing' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk invoice synchronization
|
|
*/
|
|
public function bulk_invoice_synchronization($invoice_ids, $options = [])
|
|
{
|
|
try {
|
|
$batch_size = $options['batch_size'] ?? 25;
|
|
$batches = array_chunk($invoice_ids, $batch_size);
|
|
|
|
$results = ['total_batches' => count($batches), 'results' => []];
|
|
|
|
foreach ($batches as $batch_index => $batch_invoices) {
|
|
$batch_result = $this->sync_perfex_to_moloni_bulk(['invoice_ids' => $batch_invoices]);
|
|
|
|
$results['results'][] = [
|
|
'batch' => $batch_index + 1,
|
|
'invoice_count' => count($batch_invoices),
|
|
'result' => $batch_result
|
|
];
|
|
|
|
// Add delay between batches
|
|
if (isset($options['batch_delay'])) {
|
|
sleep($options['batch_delay']);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'bulk_sync_results' => $results,
|
|
'bulk_synchronization' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transaction rollback for invoice operations
|
|
*/
|
|
public function rollback_invoice_transaction($invoice_id, $transaction_data)
|
|
{
|
|
try {
|
|
log_message('info', "Rolling back invoice transaction for invoice {$invoice_id}");
|
|
|
|
// Restore original invoice data
|
|
if (isset($transaction_data['original_data'])) {
|
|
$this->CI->load->model('invoices_model');
|
|
$this->CI->invoices_model->update($transaction_data['original_data'], $invoice_id);
|
|
}
|
|
|
|
// Remove failed mapping
|
|
if (isset($transaction_data['mapping_id'])) {
|
|
$this->mapping_model->delete($transaction_data['mapping_id']);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'rollback_completed' => true,
|
|
'transaction_rollback' => true
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->sanitize_error_message($e->getMessage())
|
|
];
|
|
}
|
|
}
|
|
} |