diff --git a/create_tables.php b/create_tables.php index 4458c3f..90ed349 100644 --- a/create_tables.php +++ b/create_tables.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + + +## Core Principles + +### [PRINCIPLE_1_NAME] + +[PRINCIPLE_1_DESCRIPTION] + + +### [PRINCIPLE_2_NAME] + +[PRINCIPLE_2_DESCRIPTION] + + +### [PRINCIPLE_3_NAME] + +[PRINCIPLE_3_DESCRIPTION] + + +### [PRINCIPLE_4_NAME] + +[PRINCIPLE_4_DESCRIPTION] + + +### [PRINCIPLE_5_NAME] + +[PRINCIPLE_5_DESCRIPTION] + + +## [SECTION_2_NAME] + + +[SECTION_2_CONTENT] + + +## [SECTION_3_NAME] + + +[SECTION_3_CONTENT] + + +## Governance + + +[GOVERNANCE_RULES] + + +**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] + \ No newline at end of file diff --git a/memory/constitution_update_checklist.md b/memory/constitution_update_checklist.md new file mode 100644 index 0000000..7f15d7f --- /dev/null +++ b/memory/constitution_update_checklist.md @@ -0,0 +1,85 @@ +# Constitution Update Checklist + +When amending the constitution (`/memory/constitution.md`), ensure all dependent documents are updated to maintain consistency. + +## Templates to Update + +### When adding/modifying ANY article: +- [ ] `/templates/plan-template.md` - Update Constitution Check section +- [ ] `/templates/spec-template.md` - Update if requirements/scope affected +- [ ] `/templates/tasks-template.md` - Update if new task types needed +- [ ] `/.claude/commands/plan.md` - Update if planning process changes +- [ ] `/.claude/commands/tasks.md` - Update if task generation affected +- [ ] `/CLAUDE.md` - Update runtime development guidelines + +### Article-specific updates: + +#### Article I (Library-First): +- [ ] Ensure templates emphasize library creation +- [ ] Update CLI command examples +- [ ] Add llms.txt documentation requirements + +#### Article II (CLI Interface): +- [ ] Update CLI flag requirements in templates +- [ ] Add text I/O protocol reminders + +#### Article III (Test-First): +- [ ] Update test order in all templates +- [ ] Emphasize TDD requirements +- [ ] Add test approval gates + +#### Article IV (Integration Testing): +- [ ] List integration test triggers +- [ ] Update test type priorities +- [ ] Add real dependency requirements + +#### Article V (Observability): +- [ ] Add logging requirements to templates +- [ ] Include multi-tier log streaming +- [ ] Update performance monitoring sections + +#### Article VI (Versioning): +- [ ] Add version increment reminders +- [ ] Include breaking change procedures +- [ ] Update migration requirements + +#### Article VII (Simplicity): +- [ ] Update project count limits +- [ ] Add pattern prohibition examples +- [ ] Include YAGNI reminders + +## Validation Steps + +1. **Before committing constitution changes:** + - [ ] All templates reference new requirements + - [ ] Examples updated to match new rules + - [ ] No contradictions between documents + +2. **After updating templates:** + - [ ] Run through a sample implementation plan + - [ ] Verify all constitution requirements addressed + - [ ] Check that templates are self-contained (readable without constitution) + +3. **Version tracking:** + - [ ] Update constitution version number + - [ ] Note version in template footers + - [ ] Add amendment to constitution history + +## Common Misses + +Watch for these often-forgotten updates: +- Command documentation (`/commands/*.md`) +- Checklist items in templates +- Example code/commands +- Domain-specific variations (web vs mobile vs CLI) +- Cross-references between documents + +## Template Sync Status + +Last sync check: 2025-07-16 +- Constitution version: 2.1.1 +- Templates aligned: ❌ (missing versioning, observability details) + +--- + +*This checklist ensures the constitution's principles are consistently applied across all project documentation.* \ No newline at end of file diff --git a/modules/desk_moloni/assets/css/admin.css b/modules/desk_moloni/assets/css/admin.css index 3c3319e..4f6e42b 100644 --- a/modules/desk_moloni/assets/css/admin.css +++ b/modules/desk_moloni/assets/css/admin.css @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + /** * Desk-Moloni Admin CSS v3.0 * diff --git a/modules/desk_moloni/assets/css/client.css b/modules/desk_moloni/assets/css/client.css index 0858b08..8dc1c50 100644 --- a/modules/desk_moloni/assets/css/client.css +++ b/modules/desk_moloni/assets/css/client.css @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + /** * Desk-Moloni Client Portal CSS * Version: 3.0.0 diff --git a/modules/desk_moloni/assets/js/admin.js b/modules/desk_moloni/assets/js/admin.js index b602c3b..5f1b755 100644 --- a/modules/desk_moloni/assets/js/admin.js +++ b/modules/desk_moloni/assets/js/admin.js @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + /** * Desk-Moloni Admin JavaScript v3.0 * diff --git a/modules/desk_moloni/assets/js/queue_management.js b/modules/desk_moloni/assets/js/queue_management.js index d96b7df..b48b0f6 100644 --- a/modules/desk_moloni/assets/js/queue_management.js +++ b/modules/desk_moloni/assets/js/queue_management.js @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + /** * Desk-Moloni Queue Management JavaScript * Handles queue operations and real-time updates diff --git a/modules/desk_moloni/config/autoload.php b/modules/desk_moloni/config/autoload.php index 2447d25..18a1089 100644 --- a/modules/desk_moloni/config/autoload.php +++ b/modules/desk_moloni/config/autoload.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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 = [ diff --git a/modules/desk_moloni/libraries/DocumentAccessControl.php b/modules/desk_moloni/libraries/DocumentAccessControl.php index 20e8658..dd432cd 100644 --- a/modules/desk_moloni/libraries/DocumentAccessControl.php +++ b/modules/desk_moloni/libraries/DocumentAccessControl.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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'); } /** diff --git a/modules/desk_moloni/libraries/ProductSyncService.php b/modules/desk_moloni/libraries/ProductSyncService.php index 13fc626..9870363 100644 --- a/modules/desk_moloni/libraries/ProductSyncService.php +++ b/modules/desk_moloni/libraries/ProductSyncService.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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'); } /** diff --git a/modules/desk_moloni/libraries/RetryHandler.php b/modules/desk_moloni/libraries/RetryHandler.php index b28bcb3..8304542 100644 --- a/modules/desk_moloni/libraries/RetryHandler.php +++ b/modules/desk_moloni/libraries/RetryHandler.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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'; + } +} diff --git a/modules/desk_moloni/models/Config_model.php b/modules/desk_moloni/models/Config_model.php index c1c5923..ded8052 100644 --- a/modules/desk_moloni/models/Config_model.php +++ b/modules/desk_moloni/models/Config_model.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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 diff --git a/modules/desk_moloni/tests/MoloniApiContractTest.php b/modules/desk_moloni/tests/MoloniApiContractTest.php index f056570..649b43e 100644 --- a/modules/desk_moloni/tests/MoloniApiContractTest.php +++ b/modules/desk_moloni/tests/MoloniApiContractTest.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + 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(); - } -} \ No newline at end of file diff --git a/modules/desk_moloni/tests/Unit/mappers/CustomerMapperTest.php b/modules/desk_moloni/tests/Unit/mappers/CustomerMapperTest.php new file mode 100644 index 0000000..9185e25 --- /dev/null +++ b/modules/desk_moloni/tests/Unit/mappers/CustomerMapperTest.php @@ -0,0 +1,90 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + +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']); + } +} diff --git a/modules/desk_moloni/tests/bootstrap.php b/modules/desk_moloni/tests/bootstrap.php index a2e2ed3..42b8a88 100644 --- a/modules/desk_moloni/tests/bootstrap.php +++ b/modules/desk_moloni/tests/bootstrap.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +