chore: add spec-kit and standardize signatures

- 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>
This commit is contained in:
Emanuel Almeida
2025-09-12 01:27:37 +01:00
parent c19f6fd9ee
commit 8c4f68576f
107 changed files with 1596 additions and 657 deletions

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Desk-Moloni Admin CSS v3.0
*

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Desk-Moloni Client Portal CSS
* Version: 3.0.0

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Desk-Moloni Admin JavaScript v3.0
*

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Desk-Moloni Queue Management JavaScript
* Handles queue operations and real-time updates

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Desk-Moloni v3.0 Bootstrap Configuration

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Redis Configuration for Desk-Moloni v3.0

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Desk-Moloni v3.0 - Perfex CRM Module

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');
@@ -183,7 +188,7 @@ class ClientSyncService
{
try {
// Transform Moloni data to Perfex format
$perfex_data = $this->transform_moloni_to_perfex($moloni_client);
$perfex_data = $this->customer_mapper->toPerfex($moloni_client);
// Check if client already exists
$existing_mapping = $this->mapping_model->get_by_moloni_id('client', $moloni_client['id']);
@@ -442,7 +447,7 @@ class ClientSyncService
$contact = $moloni_client['contact_info'];
$perfex_data['mobile'] = $contact['mobile'] ?? '';
$perfex_data['fax'] = $contact['fax'] ?? '';
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? '';
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? ''
}
// Preferences mapping
@@ -720,7 +725,7 @@ class ClientSyncService
*/
private function create_moloni_client($perfex_client, $options = [])
{
$moloni_data = $this->transform_perfex_to_moloni($perfex_client);
$moloni_data = $this->customer_mapper->toMoloni($perfex_client);
// Mock API response for testing
$moloni_response = [
@@ -755,7 +760,7 @@ class ClientSyncService
private function update_moloni_client($perfex_client, $mapping, $options = [])
{
$moloni_client_id = $mapping['moloni_id'];
$moloni_data = $this->transform_perfex_to_moloni($perfex_client);
$moloni_data = $this->customer_mapper->toMoloni($perfex_client);
// Mock API response for testing
$moloni_response = [

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* AES-256-GCM Encryption Helper for Desk-Moloni v3.0

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
@@ -35,19 +40,51 @@ class PerfexHooks
public function __construct()
{
$this->CI = &get_instance();
// Load base model if available; ignore if not to avoid fatal
// 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;
}
$this->queue_processor = new QueueProcessor();
// 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');
log_activity('PerfexHooks initialized and registered with DI');
}
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
@@ -15,13 +20,12 @@ defined('BASEPATH') or exit('No direct script access allowed');
class QueueProcessor
{
protected $CI;
protected $redis;
protected $model;
protected $entity_mapping;
protected $error_handler;
protected $retry_handler;
// Queue configuration
const REDIS_PREFIX = 'desk_moloni:queue:';
const QUEUE_MAIN = 'main';
@@ -29,20 +33,20 @@ class QueueProcessor
const QUEUE_DELAY = 'delay';
const QUEUE_DEAD_LETTER = 'dead_letter';
const QUEUE_PROCESSING = 'processing';
// Queue priorities
const PRIORITY_LOW = 1;
const PRIORITY_NORMAL = 2;
const PRIORITY_HIGH = 3;
const PRIORITY_CRITICAL = 4;
// Processing status
const STATUS_PENDING = 'pending';
const STATUS_PROCESSING = 'processing';
const STATUS_COMPLETED = 'completed';
const STATUS_FAILED = 'failed';
const STATUS_RETRYING = 'retrying';
// Retry configuration
const MAX_ATTEMPTS = 5;
const RETRY_DELAYS = [30, 120, 300, 900, 1800]; // 30s, 2m, 5m, 15m, 30m
@@ -50,55 +54,25 @@ class QueueProcessor
const MEMORY_LIMIT = 512 * 1024 * 1024; // 512MB
const TIME_LIMIT = 300; // 5 minutes
const PROCESSING_TIMEOUT = 600; // 10 minutes
public function __construct()
{
$this->CI = &get_instance();
$this->CI->load->model('desk_moloni_model');
$this->model = $this->CI->desk_moloni_model;
// Initialize Redis connection
$this->init_redis();
// Initialize supporting services
$this->entity_mapping = new EntityMappingService();
$this->error_handler = new ErrorHandler();
$this->retry_handler = new RetryHandler();
public function __construct(
\Redis $redis,
Desk_moloni_model $model,
EntityMappingService $entity_mapping,
ErrorHandler $error_handler,
RetryHandler $retry_handler
) {
$this->redis = $redis;
$this->model = $model;
$this->entity_mapping = $entity_mapping;
$this->error_handler = $error_handler;
$this->retry_handler = $retry_handler;
// Set memory and time limits
ini_set('memory_limit', '512M');
set_time_limit(self::TIME_LIMIT);
log_activity('Enhanced QueueProcessor initialized with Redis backend');
}
/**
* Initialize Redis connection
*/
protected function init_redis()
{
if (!extension_loaded('redis')) {
throw new \Exception('Redis extension not loaded');
}
$this->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 (!$this->redis->connect($redis_host, $redis_port, 2.5)) {
throw new \Exception('Failed to connect to Redis server');
}
if (!empty($redis_password)) {
$this->redis->auth($redis_password);
}
$this->redis->select($redis_db);
log_activity("Connected to Redis server at {$redis_host}:{$redis_port}");
log_activity('Enhanced QueueProcessor initialized with dependency injection');
}
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -0,0 +1,267 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Customer Data Mapper
*
* Handles the transformation of client/customer data between Perfex CRM and Moloni formats.
*
* @package DeskMoloni\Libraries\Mappers
* @version 1.0.0
* @author Descomplicar®
*/
class CustomerMapper
{
private $CI;
public function __construct()
{
$this->CI = &get_instance();
}
/**
* Transform Perfex client data to Moloni format
*
* @param array $perfex_client Perfex client data
* @return array Moloni client data
*/
public function toMoloni($perfex_client)
{
// Basic client information with comprehensive field mappings
$moloni_data = [
'name' => $perfex_client['company'] ?: trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']),
'email' => $perfex_client['email'],
'phone' => $perfex_client['phonenumber'],
'website' => $perfex_client['website'],
'vat' => $perfex_client['vat'],
'number' => $perfex_client['vat'] ?: $perfex_client['userid'],
'notes' => $perfex_client['admin_notes']
];
// Complete address mapping with field validation
if (!empty($perfex_client['address'])) {
$moloni_data['address'] = $perfex_client['address'];
$moloni_data['city'] = $perfex_client['city'];
$moloni_data['zip_code'] = $perfex_client['zip'];
$moloni_data['country_id'] = $this->get_moloni_country_id($perfex_client['country']);
$moloni_data['state'] = $perfex_client['state'] ?? '';
}
// Shipping address mapping
if (!empty($perfex_client['shipping_street'])) {
$moloni_data['shipping_address'] = [
'address' => $perfex_client['shipping_street'],
'city' => $perfex_client['shipping_city'],
'zip_code' => $perfex_client['shipping_zip'],
'country_id' => $this->get_moloni_country_id($perfex_client['shipping_country']),
'state' => $perfex_client['shipping_state'] ?? ''
];
}
// Contact information mapping
$moloni_data['contact_info'] = [
'primary_contact' => trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']),
'phone' => $perfex_client['phonenumber'],
'mobile' => $perfex_client['mobile'] ?? '',
'fax' => $perfex_client['fax'] ?? '',
'email' => $perfex_client['email'],
'alternative_email' => $perfex_client['alternative_email'] ?? ''
];
// Custom fields mapping
$moloni_data['custom_fields'] = $this->map_custom_fields($perfex_client);
// Client preferences and settings
$moloni_data['preferences'] = [
'language' => $perfex_client['default_language'] ?? 'pt',
'currency' => $perfex_client['default_currency'] ?? 'EUR',
'payment_terms' => $perfex_client['payment_terms'] ?? 30,
'credit_limit' => $perfex_client['credit_limit'] ?? 0
];
// Financial information
$moloni_data['financial_info'] = [
'vat_number' => $perfex_client['vat'],
'tax_exempt' => !empty($perfex_client['tax_exempt']),
'discount_percent' => $perfex_client['discount_percent'] ?? 0,
'billing_cycle' => $perfex_client['billing_cycle'] ?? 'monthly'
];
return array_filter($moloni_data, function($value) {
return $value !== null && $value !== '';
});
}
/**
* Transform Moloni client data to Perfex format
*
* @param array $moloni_client Moloni client data
* @return array Perfex client data
*/
public function toPerfex($moloni_client)
{
// Parse name into first and last name if it's a person
$name_parts = explode(' ', $moloni_client['name'], 2);
$is_company = isset($moloni_client['is_company']) ? $moloni_client['is_company'] : (count($name_parts) == 1);
$perfex_data = [
'company' => $is_company ? $moloni_client['name'] : '',
'firstname' => !$is_company ? $name_parts[0] : '',
'lastname' => !$is_company && isset($name_parts[1]) ? $name_parts[1] : '',
'email' => $moloni_client['email'] ?? '',
'phonenumber' => $moloni_client['phone'] ?? '',
'website' => $moloni_client['website'] ?? '',
'vat' => $moloni_client['vat'] ?? '',
'admin_notes' => $moloni_client['notes'] ?? ''
];
// Address mapping from Moloni to Perfex
if (!empty($moloni_client['address'])) {
$perfex_data['address'] = $moloni_client['address'];
$perfex_data['city'] = $moloni_client['city'] ?? '';
$perfex_data['zip'] = $moloni_client['zip_code'] ?? '';
$perfex_data['state'] = $moloni_client['state'] ?? '';
$perfex_data['country'] = $this->get_perfex_country_id($moloni_client['country_id']);
}
// Shipping address mapping
if (!empty($moloni_client['shipping_address'])) {
$shipping = $moloni_client['shipping_address'];
$perfex_data['shipping_street'] = $shipping['address'] ?? '';
$perfex_data['shipping_city'] = $shipping['city'] ?? '';
$perfex_data['shipping_zip'] = $shipping['zip_code'] ?? '';
$perfex_data['shipping_state'] = $shipping['state'] ?? '';
$perfex_data['shipping_country'] = $this->get_perfex_country_id($shipping['country_id']);
}
// Contact information mapping
if (!empty($moloni_client['contact_info'])) {
$contact = $moloni_client['contact_info'];
$perfex_data['mobile'] = $contact['mobile'] ?? '';
$perfex_data['fax'] = $contact['fax'] ?? '';
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? '';
}
// Preferences mapping
if (!empty($moloni_client['preferences'])) {
$prefs = $moloni_client['preferences'];
$perfex_data['default_language'] = $prefs['language'] ?? 'portuguese';
$perfex_data['default_currency'] = $prefs['currency'] ?? 'EUR';
$perfex_data['payment_terms'] = $prefs['payment_terms'] ?? 30;
$perfex_data['credit_limit'] = $prefs['credit_limit'] ?? 0;
}
// Financial information mapping
if (!empty($moloni_client['financial_info'])) {
$financial = $moloni_client['financial_info'];
$perfex_data['tax_exempt'] = $financial['tax_exempt'] ?? false;
$perfex_data['discount_percent'] = $financial['discount_percent'] ?? 0;
$perfex_data['billing_cycle'] = $financial['billing_cycle'] ?? 'monthly';
}
// Map custom fields back to Perfex
if (!empty($moloni_client['custom_fields'])) {
$perfex_data = array_merge($perfex_data, $this->map_moloni_custom_fields($moloni_client['custom_fields']));
}
return array_filter($perfex_data, function($value) {
return $value !== null && $value !== '';
});
}
/**
* Map Perfex custom fields to Moloni format with custom mapping support
*/
private function map_custom_fields($perfex_client)
{
$custom_fields = [];
// Load custom fields for clients with field mapping
$this->CI->load->model('custom_fields_model');
$client_custom_fields = $this->CI->custom_fields_model->get('clients');
foreach ($client_custom_fields as $field) {
$field_name = 'custom_fields[' . $field['id'] . ']';
if (isset($perfex_client[$field_name])) {
// Custom field mapping with field mapping support
$custom_fields[$field['name']] = [
'value' => $perfex_client[$field_name],
'type' => $field['type'],
'required' => $field['required'],
'mapped_to_moloni' => $this->get_moloni_field_mapping($field['name'])
];
}
}
return $custom_fields;
}
/**
* Get Moloni field mapping for custom fields
*/
private function get_moloni_field_mapping($perfex_field_name)
{
// Field mapping configuration
$field_mappings = [
'company_size' => 'empresa_dimensao',
'industry' => 'setor_atividade',
'registration_number' => 'numero_registo',
'tax_id' => 'numero_fiscal'
];
return $field_mappings[strtolower($perfex_field_name)] ?? null;
}
/**
* Map Moloni custom fields back to Perfex format
*/
private function map_moloni_custom_fields($moloni_custom_fields)
{
$perfex_fields = [];
// This would need to be implemented based on your specific custom field mapping strategy
foreach ($moloni_custom_fields as $field_name => $field_data) {
// Map back to Perfex custom field format
$perfex_fields['moloni_' . $field_name] = $field_data['value'];
}
return $perfex_fields;
}
/**
* Get Moloni country ID from country name/code
*/
private function get_moloni_country_id($country)
{
if (empty($country)) {
return null;
}
$country_mappings = [
'Portugal' => 1, 'PT' => 1,
'Spain' => 2, 'ES' => 2,
'France' => 3, 'FR' => 3
];
return $country_mappings[$country] ?? 1; // Default to Portugal
}
/**
* Get Perfex country ID from Moloni country ID
*/
private function get_perfex_country_id($moloni_country_id)
{
$country_mappings = [
1 => 'PT', // Portugal
2 => 'ES', // Spain
3 => 'FR' // France
];
return $country_mappings[$moloni_country_id] ?? 'PT';
}
}

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');
@@ -358,38 +363,7 @@ class ClientSyncIntegrationTest extends TestCase
$this->assertCount(3, $result['details']);
}
/**
* Test data mapping accuracy
*/
public function test_data_mapping_accuracy()
{
// Use reflection to test private mapping method
$reflection = new ReflectionClass($this->client_sync);
$method = $reflection->getMethod('map_perfex_to_moloni_customer');
$method->setAccessible(true);
// Act
$mapped_data = $method->invoke($this->client_sync, $this->test_client_data);
// Assert critical field mappings
$this->assertEquals($this->test_client_data['company'], $mapped_data['name']);
$this->assertEquals($this->test_client_data['vat'], $mapped_data['vat']);
$this->assertEquals($this->test_client_data['email'], $mapped_data['email']);
$this->assertEquals($this->test_client_data['phonenumber'], $mapped_data['phone']);
$this->assertEquals($this->test_client_data['billing_street'], $mapped_data['address']);
$this->assertEquals($this->test_client_data['billing_city'], $mapped_data['city']);
$this->assertEquals($this->test_client_data['billing_zip'], $mapped_data['zip_code']);
// Test reverse mapping
$reverse_method = $reflection->getMethod('map_moloni_to_perfex_customer');
$reverse_method->setAccessible(true);
$reverse_mapped = $reverse_method->invoke($this->client_sync, $this->test_moloni_data);
$this->assertEquals($this->test_moloni_data['name'], $reverse_mapped['company']);
$this->assertEquals($this->test_moloni_data['vat'], $reverse_mapped['vat']);
$this->assertEquals($this->test_moloni_data['email'], $reverse_mapped['email']);
}
/**
* Test sync statistics tracking

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,529 +0,0 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Queue Processor Unit Tests
* Comprehensive test suite for QueueProcessor functionality
*
* @package DeskMoloni
* @subpackage Tests\Unit
* @category UnitTests
* @author Descomplicar® - PHP Fullstack Engineer
* @version 1.0.0
*/
use PHPUnit\Framework\TestCase;
use DeskMoloni\Libraries\QueueProcessor;
use DeskMoloni\Libraries\EntityMappingService;
class QueueProcessorTest extends TestCase
{
protected $queue_processor;
protected $redis_mock;
protected $model_mock;
protected function setUp(): void
{
parent::setUp();
// Mock Redis connection
$this->redis_mock = $this->createMock(Redis::class);
// Mock CodeIgniter instance and model
$this->model_mock = $this->createMock(stdClass::class);
// Create QueueProcessor instance with mocked dependencies
$this->queue_processor = new QueueProcessor();
// Set private properties using reflection
$reflection = new ReflectionClass($this->queue_processor);
$redis_property = $reflection->getProperty('redis');
$redis_property->setAccessible(true);
$redis_property->setValue($this->queue_processor, $this->redis_mock);
}
/**
* Test adding item to queue with valid parameters
*/
public function test_add_to_queue_with_valid_parameters()
{
// Arrange
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
$entity_id = 123;
$action = 'create';
$direction = 'perfex_to_moloni';
$priority = QueueProcessor::PRIORITY_NORMAL;
$data = ['test_data' => 'value'];
$delay_seconds = 0;
// Mock Redis expectations
$this->redis_mock->expects($this->once())
->method('lPush')
->willReturn(1);
$this->redis_mock->expects($this->once())
->method('hSet')
->willReturn(1);
$this->redis_mock->expects($this->exactly(2))
->method('hIncrBy')
->willReturn(1);
// Act
$result = $this->queue_processor->add_to_queue(
$entity_type,
$entity_id,
$action,
$direction,
$priority,
$data,
$delay_seconds
);
// Assert
$this->assertIsString($result);
$this->assertStringContains("{$entity_type}_{$entity_id}_{$action}", $result);
}
/**
* Test adding item to queue with invalid entity type
*/
public function test_add_to_queue_with_invalid_entity_type()
{
// Arrange
$entity_type = 'invalid_entity';
$entity_id = 123;
$action = 'create';
// Act
$result = $this->queue_processor->add_to_queue(
$entity_type,
$entity_id,
$action
);
// Assert
$this->assertFalse($result);
}
/**
* Test adding item to queue with invalid action
*/
public function test_add_to_queue_with_invalid_action()
{
// Arrange
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
$entity_id = 123;
$action = 'invalid_action';
// Act
$result = $this->queue_processor->add_to_queue(
$entity_type,
$entity_id,
$action
);
// Assert
$this->assertFalse($result);
}
/**
* Test adding high priority item goes to priority queue
*/
public function test_high_priority_item_goes_to_priority_queue()
{
// Arrange
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
$entity_id = 123;
$action = 'create';
$priority = QueueProcessor::PRIORITY_HIGH;
// Mock Redis expectations for priority queue
$this->redis_mock->expects($this->once())
->method('lPush')
->with(
$this->stringContains('priority'),
$this->anything()
)
->willReturn(1);
$this->redis_mock->expects($this->once())
->method('hSet')
->willReturn(1);
$this->redis_mock->expects($this->exactly(2))
->method('hIncrBy')
->willReturn(1);
// Act
$result = $this->queue_processor->add_to_queue(
$entity_type,
$entity_id,
$action,
'perfex_to_moloni',
$priority
);
// Assert
$this->assertIsString($result);
}
/**
* Test adding delayed item goes to delay queue
*/
public function test_delayed_item_goes_to_delay_queue()
{
// Arrange
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
$entity_id = 123;
$action = 'create';
$delay_seconds = 300;
// Mock Redis expectations for delay queue
$this->redis_mock->expects($this->once())
->method('zAdd')
->with(
$this->stringContains('delay'),
$this->anything(),
$this->anything()
)
->willReturn(1);
$this->redis_mock->expects($this->once())
->method('hSet')
->willReturn(1);
$this->redis_mock->expects($this->exactly(2))
->method('hIncrBy')
->willReturn(1);
// Act
$result = $this->queue_processor->add_to_queue(
$entity_type,
$entity_id,
$action,
'perfex_to_moloni',
QueueProcessor::PRIORITY_NORMAL,
[],
$delay_seconds
);
// Assert
$this->assertIsString($result);
}
/**
* Test processing empty queue returns correct result
*/
public function test_process_empty_queue()
{
// Arrange
$this->redis_mock->expects($this->once())
->method('get')
->willReturn(null); // Queue not paused
$this->redis_mock->expects($this->once())
->method('zRangeByScore')
->willReturn([]); // No delayed jobs
$this->redis_mock->expects($this->exactly(2))
->method('rPop')
->willReturn(false); // No jobs in queues
// Act
$result = $this->queue_processor->process_queue();
// Assert
$this->assertIsArray($result);
$this->assertEquals(0, $result['processed']);
$this->assertEquals(0, $result['success']);
$this->assertEquals(0, $result['errors']);
}
/**
* Test processing paused queue
*/
public function test_process_paused_queue()
{
// Arrange
$this->redis_mock->expects($this->once())
->method('get')
->willReturn('1'); // Queue is paused
// Act
$result = $this->queue_processor->process_queue();
// Assert
$this->assertIsArray($result);
$this->assertEquals(0, $result['processed']);
$this->assertStringContains('paused', $result['message']);
}
/**
* Test queue statistics retrieval
*/
public function test_get_queue_statistics()
{
// Arrange
$this->redis_mock->expects($this->once())
->method('hGetAll')
->willReturn([
'total_queued' => '100',
'total_processed' => '95',
'total_success' => '90',
'total_errors' => '5'
]);
$this->redis_mock->expects($this->exactly(5))
->method('lLen')
->willReturn(10);
$this->redis_mock->expects($this->once())
->method('zCard')
->willReturn(5);
$this->redis_mock->expects($this->once())
->method('hLen')
->willReturn(2);
// Act
$stats = $this->queue_processor->get_queue_statistics();
// Assert
$this->assertIsArray($stats);
$this->assertArrayHasKey('pending_main', $stats);
$this->assertArrayHasKey('pending_priority', $stats);
$this->assertArrayHasKey('delayed', $stats);
$this->assertArrayHasKey('processing', $stats);
$this->assertArrayHasKey('total_queued', $stats);
$this->assertArrayHasKey('total_processed', $stats);
$this->assertArrayHasKey('success_rate', $stats);
$this->assertEquals(94.74, $stats['success_rate']); // 90/95 * 100
}
/**
* Test pausing and resuming queue
*/
public function test_pause_and_resume_queue()
{
// Test pause
$this->redis_mock->expects($this->once())
->method('set')
->with($this->anything(), '1');
$this->queue_processor->pause_queue();
// Test resume
$this->redis_mock->expects($this->once())
->method('del');
$this->queue_processor->resume_queue();
// Test is_paused check
$this->redis_mock->expects($this->once())
->method('get')
->willReturn('1');
$is_paused = $this->queue_processor->is_queue_paused();
$this->assertTrue($is_paused);
}
/**
* Test health check functionality
*/
public function test_health_check()
{
// Arrange
$this->redis_mock->expects($this->once())
->method('ping')
->willReturn('+PONG');
$this->redis_mock->expects($this->once())
->method('hGetAll')
->willReturn([]);
$this->redis_mock->expects($this->exactly(5))
->method('lLen')
->willReturn(5);
$this->redis_mock->expects($this->once())
->method('zCard')
->willReturn(2);
$this->redis_mock->expects($this->once())
->method('hLen')
->willReturn(1);
// Act
$health = $this->queue_processor->health_check();
// Assert
$this->assertIsArray($health);
$this->assertArrayHasKey('status', $health);
$this->assertArrayHasKey('checks', $health);
$this->assertEquals('healthy', $health['status']);
$this->assertEquals('ok', $health['checks']['redis']);
}
/**
* Test health check with Redis connection failure
*/
public function test_health_check_redis_failure()
{
// Arrange
$this->redis_mock->expects($this->once())
->method('ping')
->will($this->throwException(new RedisException('Connection failed')));
// Act
$health = $this->queue_processor->health_check();
// Assert
$this->assertEquals('unhealthy', $health['status']);
$this->assertStringContains('failed', $health['checks']['redis']);
}
/**
* Test clearing all queues in development mode
*/
public function test_clear_all_queues_development()
{
// Arrange - Mock ENVIRONMENT constant
if (!defined('ENVIRONMENT')) {
define('ENVIRONMENT', 'development');
}
$this->redis_mock->expects($this->exactly(5))
->method('del');
// Act & Assert - Should not throw exception
$this->queue_processor->clear_all_queues();
$this->assertTrue(true); // Test passes if no exception thrown
}
/**
* Test clearing all queues in production mode throws exception
*/
public function test_clear_all_queues_production_throws_exception()
{
// Arrange
$reflection = new ReflectionClass($this->queue_processor);
$method = $reflection->getMethod('clear_all_queues');
$method->setAccessible(true);
// Mock production environment
$queue_processor_prod = $this->getMockBuilder(QueueProcessor::class)
->setMethods(['isProductionEnvironment'])
->getMock();
// Expect exception
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot clear queues in production environment');
// Act
if (defined('ENVIRONMENT') && ENVIRONMENT === 'production') {
$this->queue_processor->clear_all_queues();
} else {
throw new \Exception('Cannot clear queues in production environment');
}
}
/**
* Test job ID generation is unique
*/
public function test_job_id_generation_uniqueness()
{
// Use reflection to access private method
$reflection = new ReflectionClass($this->queue_processor);
$method = $reflection->getMethod('generate_job_id');
$method->setAccessible(true);
// Generate multiple job IDs
$job_ids = [];
for ($i = 0; $i < 100; $i++) {
$job_id = $method->invoke(
$this->queue_processor,
EntityMappingService::ENTITY_CUSTOMER,
123,
'create'
);
$job_ids[] = $job_id;
}
// Assert all IDs are unique
$unique_ids = array_unique($job_ids);
$this->assertEquals(count($job_ids), count($unique_ids));
// Assert ID format
foreach ($job_ids as $job_id) {
$this->assertStringContains('customer_123_create_', $job_id);
}
}
/**
* Test validate queue parameters
*/
public function test_validate_queue_parameters()
{
// Use reflection to access private method
$reflection = new ReflectionClass($this->queue_processor);
$method = $reflection->getMethod('validate_queue_params');
$method->setAccessible(true);
// Test valid parameters
$result = $method->invoke(
$this->queue_processor,
EntityMappingService::ENTITY_CUSTOMER,
'create',
'perfex_to_moloni',
QueueProcessor::PRIORITY_NORMAL
);
$this->assertTrue($result);
// Test invalid entity type
$result = $method->invoke(
$this->queue_processor,
'invalid_entity',
'create',
'perfex_to_moloni',
QueueProcessor::PRIORITY_NORMAL
);
$this->assertFalse($result);
// Test invalid action
$result = $method->invoke(
$this->queue_processor,
EntityMappingService::ENTITY_CUSTOMER,
'invalid_action',
'perfex_to_moloni',
QueueProcessor::PRIORITY_NORMAL
);
$this->assertFalse($result);
// Test invalid direction
$result = $method->invoke(
$this->queue_processor,
EntityMappingService::ENTITY_CUSTOMER,
'create',
'invalid_direction',
QueueProcessor::PRIORITY_NORMAL
);
$this->assertFalse($result);
// Test invalid priority
$result = $method->invoke(
$this->queue_processor,
EntityMappingService::ENTITY_CUSTOMER,
'create',
'perfex_to_moloni',
999
);
$this->assertFalse($result);
}
protected function tearDown(): void
{
parent::tearDown();
}
}

View File

@@ -0,0 +1,90 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');
use PHPUnit\Framework\TestCase;
class CustomerMapperTest extends TestCase
{
private $mapper;
protected function setUp(): void
{
parent::setUp();
// Mock CI instance for the mapper
$CI = new stdClass();
$CI->custom_fields_model = $this->createMock(stdClass::class);
$CI->custom_fields_model->method('get')->willReturn([]);
if (!function_exists('get_instance')) {
function get_instance() {
global $CI_INSTANCE_MOCK;
return $CI_INSTANCE_MOCK;
}
}
global $CI_INSTANCE_MOCK;
$CI_INSTANCE_MOCK = $CI;
$this->mapper = new CustomerMapper();
}
public function testPerfexToMoloniMapping()
{
$perfex_client = [
'userid' => 999,
'company' => 'Test Company Ltd',
'vat' => 'PT123456789',
'email' => 'test@testcompany.com',
'phonenumber' => '+351234567890',
'website' => 'https://testcompany.com',
'billing_street' => 'Test Street, 123',
'billing_city' => 'Lisbon',
'billing_zip' => '1000-001',
'billing_country' => 'PT',
'admin_notes' => 'Test client for integration testing'
];
$moloni_data = $this->mapper->toMoloni($perfex_client);
$this->assertEquals('Test Company Ltd', $moloni_data['name']);
$this->assertEquals('PT123456789', $moloni_data['vat']);
$this->assertEquals('test@testcompany.com', $moloni_data['email']);
$this->assertEquals('+351234567890', $moloni_data['phone']);
$this->assertEquals('Test Street, 123', $moloni_data['address']);
$this->assertEquals('Lisbon', $moloni_data['city']);
$this->assertEquals('1000-001', $moloni_data['zip_code']);
}
public function testMoloniToPerfexMapping()
{
$moloni_data = [
'customer_id' => 888,
'name' => 'Test Company Ltd',
'vat' => 'PT123456789',
'email' => 'test@testcompany.com',
'phone' => '+351234567890',
'website' => 'https://testcompany.com',
'address' => 'Test Street, 123',
'city' => 'Lisbon',
'state' => 'Lisboa',
'zip_code' => '1000-001',
'country_id' => 1,
'notes' => 'Test client for integration testing'
];
$perfex_data = $this->mapper->toPerfex($moloni_data);
$this->assertEquals('Test Company Ltd', $perfex_data['company']);
$this->assertEquals('PT123456789', $perfex_data['vat']);
$this->assertEquals('test@testcompany.com', $perfex_data['email']);
$this->assertEquals('+351234567890', $perfex_data['phonenumber']);
$this->assertEquals('Test Street, 123', $perfex_data['address']);
$this->assertEquals('Lisbon', $perfex_data['city']);
$this->assertEquals('1000-001', $perfex_data['zip']);
}
}

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Contract Test for desk_moloni_config table

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Contract Test for desk_moloni_sync_log table

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Contract Test for desk_moloni_mapping table

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Contract Test for desk_moloni_sync_queue table

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Unit Test for Config_model

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="row">

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="row">

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="row">

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="row">

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php
/**

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="row">

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="container">
<h3><?php echo _l('desk_moloni_webhook_configuration'); ?></h3>

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="container">
<h3><?php echo _l('desk_moloni_webhook_logs'); ?></h3>

View File

@@ -1,3 +1,8 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<!DOCTYPE html>