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 + */ +
diff --git a/modules/desk_moloni/views/admin/dashboard.php b/modules/desk_moloni/views/admin/dashboard.php index e18e429..1169f4f 100644 --- a/modules/desk_moloni/views/admin/dashboard.php +++ b/modules/desk_moloni/views/admin/dashboard.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +
diff --git a/modules/desk_moloni/views/admin/mapping_management.php b/modules/desk_moloni/views/admin/mapping_management.php index 0138dae..87721fe 100644 --- a/modules/desk_moloni/views/admin/mapping_management.php +++ b/modules/desk_moloni/views/admin/mapping_management.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +
diff --git a/modules/desk_moloni/views/admin/oauth_setup.php b/modules/desk_moloni/views/admin/oauth_setup.php index e362407..fdf8961 100644 --- a/modules/desk_moloni/views/admin/oauth_setup.php +++ b/modules/desk_moloni/views/admin/oauth_setup.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +
diff --git a/modules/desk_moloni/views/admin/partials/csrf_token.php b/modules/desk_moloni/views/admin/partials/csrf_token.php index 75ba47e..e000f21 100644 --- a/modules/desk_moloni/views/admin/partials/csrf_token.php +++ b/modules/desk_moloni/views/admin/partials/csrf_token.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +
diff --git a/modules/desk_moloni/views/admin/webhook_configuration.php b/modules/desk_moloni/views/admin/webhook_configuration.php index c015310..6ac4e9d 100644 --- a/modules/desk_moloni/views/admin/webhook_configuration.php +++ b/modules/desk_moloni/views/admin/webhook_configuration.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +

diff --git a/modules/desk_moloni/views/admin/webhook_logs.php b/modules/desk_moloni/views/admin/webhook_logs.php index ea8d27c..b434c78 100644 --- a/modules/desk_moloni/views/admin/webhook_logs.php +++ b/modules/desk_moloni/views/admin/webhook_logs.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +

diff --git a/modules/desk_moloni/views/client_portal/index.php b/modules/desk_moloni/views/client_portal/index.php index 9114577..b942886 100644 --- a/modules/desk_moloni/views/client_portal/index.php +++ b/modules/desk_moloni/views/client_portal/index.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + diff --git a/simple_table_creator.php b/simple_table_creator.php index c7caa29..e5e8084 100644 --- a/simple_table_creator.php +++ b/simple_table_creator.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + + \ No newline at end of file diff --git a/templates/plan-template.md b/templates/plan-template.md new file mode 100644 index 0000000..f28a655 --- /dev/null +++ b/templates/plan-template.md @@ -0,0 +1,237 @@ +# Implementation Plan: [FEATURE] + +**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` + +## Execution Flow (/plan command scope) +``` +1. Load feature spec from Input path + → If not found: ERROR "No feature spec at {path}" +2. Fill Technical Context (scan for NEEDS CLARIFICATION) + → Detect Project Type from context (web=frontend+backend, mobile=app+api) + → Set Structure Decision based on project type +3. Evaluate Constitution Check section below + → If violations exist: Document in Complexity Tracking + → If no justification possible: ERROR "Simplify approach first" + → Update Progress Tracking: Initial Constitution Check +4. Execute Phase 0 → research.md + → If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns" +5. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, or `GEMINI.md` for Gemini CLI). +6. Re-evaluate Constitution Check section + → If new violations: Refactor design, return to Phase 1 + → Update Progress Tracking: Post-Design Constitution Check +7. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md) +8. STOP - Ready for /tasks command +``` + +**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands: +- Phase 2: /tasks command creates tasks.md +- Phase 3-4: Implementation execution (manual or via tools) + +## Summary +[Extract from feature spec: primary requirement + technical approach from research] + +## Technical Context +**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] +**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] +**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] +**Project Type**: [single/web/mobile - determines source structure] +**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] +**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] +**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] + +## Constitution Check +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +**Simplicity**: +- Projects: [#] (max 3 - e.g., api, cli, tests) +- Using framework directly? (no wrapper classes) +- Single data model? (no DTOs unless serialization differs) +- Avoiding patterns? (no Repository/UoW without proven need) + +**Architecture**: +- EVERY feature as library? (no direct app code) +- Libraries listed: [name + purpose for each] +- CLI per library: [commands with --help/--version/--format] +- Library docs: llms.txt format planned? + +**Testing (NON-NEGOTIABLE)**: +- RED-GREEN-Refactor cycle enforced? (test MUST fail first) +- Git commits show tests before implementation? +- Order: Contract→Integration→E2E→Unit strictly followed? +- Real dependencies used? (actual DBs, not mocks) +- Integration tests for: new libraries, contract changes, shared schemas? +- FORBIDDEN: Implementation before test, skipping RED phase + +**Observability**: +- Structured logging included? +- Frontend logs → backend? (unified stream) +- Error context sufficient? + +**Versioning**: +- Version number assigned? (MAJOR.MINOR.BUILD) +- BUILD increments on every change? +- Breaking changes handled? (parallel tests, migration plan) + +## Project Structure + +### Documentation (this feature) +``` +specs/[###-feature]/ +├── plan.md # This file (/plan command output) +├── research.md # Phase 0 output (/plan command) +├── data-model.md # Phase 1 output (/plan command) +├── quickstart.md # Phase 1 output (/plan command) +├── contracts/ # Phase 1 output (/plan command) +└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan) +``` + +### Source Code (repository root) +``` +# Option 1: Single project (DEFAULT) +src/ +├── models/ +├── services/ +├── cli/ +└── lib/ + +tests/ +├── contract/ +├── integration/ +└── unit/ + +# Option 2: Web application (when "frontend" + "backend" detected) +backend/ +├── src/ +│ ├── models/ +│ ├── services/ +│ └── api/ +└── tests/ + +frontend/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ └── services/ +└── tests/ + +# Option 3: Mobile + API (when "iOS/Android" detected) +api/ +└── [same as backend above] + +ios/ or android/ +└── [platform-specific structure] +``` + +**Structure Decision**: [DEFAULT to Option 1 unless Technical Context indicates web/mobile app] + +## Phase 0: Outline & Research +1. **Extract unknowns from Technical Context** above: + - For each NEEDS CLARIFICATION → research task + - For each dependency → best practices task + - For each integration → patterns task + +2. **Generate and dispatch research agents**: + ``` + For each unknown in Technical Context: + Task: "Research {unknown} for {feature context}" + For each technology choice: + Task: "Find best practices for {tech} in {domain}" + ``` + +3. **Consolidate findings** in `research.md` using format: + - Decision: [what was chosen] + - Rationale: [why chosen] + - Alternatives considered: [what else evaluated] + +**Output**: research.md with all NEEDS CLARIFICATION resolved + +## Phase 1: Design & Contracts +*Prerequisites: research.md complete* + +1. **Extract entities from feature spec** → `data-model.md`: + - Entity name, fields, relationships + - Validation rules from requirements + - State transitions if applicable + +2. **Generate API contracts** from functional requirements: + - For each user action → endpoint + - Use standard REST/GraphQL patterns + - Output OpenAPI/GraphQL schema to `/contracts/` + +3. **Generate contract tests** from contracts: + - One test file per endpoint + - Assert request/response schemas + - Tests must fail (no implementation yet) + +4. **Extract test scenarios** from user stories: + - Each story → integration test scenario + - Quickstart test = story validation steps + +5. **Update agent file incrementally** (O(1) operation): + - Run `/scripts/update-agent-context.sh [claude|gemini|copilot]` for your AI assistant + - If exists: Add only NEW tech from current plan + - Preserve manual additions between markers + - Update recent changes (keep last 3) + - Keep under 150 lines for token efficiency + - Output to repository root + +**Output**: data-model.md, /contracts/*, failing tests, quickstart.md, agent-specific file + +## Phase 2: Task Planning Approach +*This section describes what the /tasks command will do - DO NOT execute during /plan* + +**Task Generation Strategy**: +- Load `/templates/tasks-template.md` as base +- Generate tasks from Phase 1 design docs (contracts, data model, quickstart) +- Each contract → contract test task [P] +- Each entity → model creation task [P] +- Each user story → integration test task +- Implementation tasks to make tests pass + +**Ordering Strategy**: +- TDD order: Tests before implementation +- Dependency order: Models before services before UI +- Mark [P] for parallel execution (independent files) + +**Estimated Output**: 25-30 numbered, ordered tasks in tasks.md + +**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan + +## Phase 3+: Future Implementation +*These phases are beyond the scope of the /plan command* + +**Phase 3**: Task execution (/tasks command creates tasks.md) +**Phase 4**: Implementation (execute tasks.md following constitutional principles) +**Phase 5**: Validation (run tests, execute quickstart.md, performance validation) + +## Complexity Tracking +*Fill ONLY if Constitution Check has violations that must be justified* + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | +| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | + + +## Progress Tracking +*This checklist is updated during execution flow* + +**Phase Status**: +- [ ] Phase 0: Research complete (/plan command) +- [ ] Phase 1: Design complete (/plan command) +- [ ] Phase 2: Task planning complete (/plan command - describe approach only) +- [ ] Phase 3: Tasks generated (/tasks command) +- [ ] Phase 4: Implementation complete +- [ ] Phase 5: Validation passed + +**Gate Status**: +- [ ] Initial Constitution Check: PASS +- [ ] Post-Design Constitution Check: PASS +- [ ] All NEEDS CLARIFICATION resolved +- [ ] Complexity deviations documented + +--- +*Based on Constitution v2.1.1 - See `/memory/constitution.md`* \ No newline at end of file diff --git a/templates/spec-template.md b/templates/spec-template.md new file mode 100644 index 0000000..7915e7d --- /dev/null +++ b/templates/spec-template.md @@ -0,0 +1,116 @@ +# Feature Specification: [FEATURE NAME] + +**Feature Branch**: `[###-feature-name]` +**Created**: [DATE] +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## Execution Flow (main) +``` +1. Parse user description from Input + → If empty: ERROR "No feature description provided" +2. Extract key concepts from description + → Identify: actors, actions, data, constraints +3. For each unclear aspect: + → Mark with [NEEDS CLARIFICATION: specific question] +4. Fill User Scenarios & Testing section + → If no clear user flow: ERROR "Cannot determine user scenarios" +5. Generate Functional Requirements + → Each requirement must be testable + → Mark ambiguous requirements +6. Identify Key Entities (if data involved) +7. Run Review Checklist + → If any [NEEDS CLARIFICATION]: WARN "Spec has uncertainties" + → If implementation details found: ERROR "Remove tech details" +8. Return: SUCCESS (spec ready for planning) +``` + +--- + +## ⚡ Quick Guidelines +- ✅ Focus on WHAT users need and WHY +- ❌ Avoid HOW to implement (no tech stack, APIs, code structure) +- 👥 Written for business stakeholders, not developers + +### Section Requirements +- **Mandatory sections**: Must be completed for every feature +- **Optional sections**: Include only when relevant to the feature +- When a section doesn't apply, remove it entirely (don't leave as "N/A") + +### For AI Generation +When creating this spec from a user prompt: +1. **Mark all ambiguities**: Use [NEEDS CLARIFICATION: specific question] for any assumption you'd need to make +2. **Don't guess**: If the prompt doesn't specify something (e.g., "login system" without auth method), mark it +3. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item +4. **Common underspecified areas**: + - User types and permissions + - Data retention/deletion policies + - Performance targets and scale + - Error handling behaviors + - Integration requirements + - Security/compliance needs + +--- + +## User Scenarios & Testing *(mandatory)* + +### Primary User Story +[Describe the main user journey in plain language] + +### Acceptance Scenarios +1. **Given** [initial state], **When** [action], **Then** [expected outcome] +2. **Given** [initial state], **When** [action], **Then** [expected outcome] + +### Edge Cases +- What happens when [boundary condition]? +- How does system handle [error scenario]? + +## Requirements *(mandatory)* + +### Functional Requirements +- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] +- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] +- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] +- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] +- **FR-005**: System MUST [behavior, e.g., "log all security events"] + +*Example of marking unclear requirements:* +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] +- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* +- **[Entity 1]**: [What it represents, key attributes without implementation] +- **[Entity 2]**: [What it represents, relationships to other entities] + +--- + +## Review & Acceptance Checklist +*GATE: Automated checks run during main() execution* + +### Content Quality +- [ ] No implementation details (languages, frameworks, APIs) +- [ ] Focused on user value and business needs +- [ ] Written for non-technical stakeholders +- [ ] All mandatory sections completed + +### Requirement Completeness +- [ ] No [NEEDS CLARIFICATION] markers remain +- [ ] Requirements are testable and unambiguous +- [ ] Success criteria are measurable +- [ ] Scope is clearly bounded +- [ ] Dependencies and assumptions identified + +--- + +## Execution Status +*Updated by main() during processing* + +- [ ] User description parsed +- [ ] Key concepts extracted +- [ ] Ambiguities marked +- [ ] User scenarios defined +- [ ] Requirements generated +- [ ] Entities identified +- [ ] Review checklist passed + +--- diff --git a/templates/tasks-template.md b/templates/tasks-template.md new file mode 100644 index 0000000..b8a28fa --- /dev/null +++ b/templates/tasks-template.md @@ -0,0 +1,127 @@ +# Tasks: [FEATURE NAME] + +**Input**: Design documents from `/specs/[###-feature-name]/` +**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/ + +## Execution Flow (main) +``` +1. Load plan.md from feature directory + → If not found: ERROR "No implementation plan found" + → Extract: tech stack, libraries, structure +2. Load optional design documents: + → data-model.md: Extract entities → model tasks + → contracts/: Each file → contract test task + → research.md: Extract decisions → setup tasks +3. Generate tasks by category: + → Setup: project init, dependencies, linting + → Tests: contract tests, integration tests + → Core: models, services, CLI commands + → Integration: DB, middleware, logging + → Polish: unit tests, performance, docs +4. Apply task rules: + → Different files = mark [P] for parallel + → Same file = sequential (no [P]) + → Tests before implementation (TDD) +5. Number tasks sequentially (T001, T002...) +6. Generate dependency graph +7. Create parallel execution examples +8. Validate task completeness: + → All contracts have tests? + → All entities have models? + → All endpoints implemented? +9. Return: SUCCESS (tasks ready for execution) +``` + +## Format: `[ID] [P?] Description` +- **[P]**: Can run in parallel (different files, no dependencies) +- Include exact file paths in descriptions + +## Path Conventions +- **Single project**: `src/`, `tests/` at repository root +- **Web app**: `backend/src/`, `frontend/src/` +- **Mobile**: `api/src/`, `ios/src/` or `android/src/` +- Paths shown below assume single project - adjust based on plan.md structure + +## Phase 3.1: Setup +- [ ] T001 Create project structure per implementation plan +- [ ] T002 Initialize [language] project with [framework] dependencies +- [ ] T003 [P] Configure linting and formatting tools + +## Phase 3.2: Tests First (TDD) ⚠️ MUST COMPLETE BEFORE 3.3 +**CRITICAL: These tests MUST be written and MUST FAIL before ANY implementation** +- [ ] T004 [P] Contract test POST /api/users in tests/contract/test_users_post.py +- [ ] T005 [P] Contract test GET /api/users/{id} in tests/contract/test_users_get.py +- [ ] T006 [P] Integration test user registration in tests/integration/test_registration.py +- [ ] T007 [P] Integration test auth flow in tests/integration/test_auth.py + +## Phase 3.3: Core Implementation (ONLY after tests are failing) +- [ ] T008 [P] User model in src/models/user.py +- [ ] T009 [P] UserService CRUD in src/services/user_service.py +- [ ] T010 [P] CLI --create-user in src/cli/user_commands.py +- [ ] T011 POST /api/users endpoint +- [ ] T012 GET /api/users/{id} endpoint +- [ ] T013 Input validation +- [ ] T014 Error handling and logging + +## Phase 3.4: Integration +- [ ] T015 Connect UserService to DB +- [ ] T016 Auth middleware +- [ ] T017 Request/response logging +- [ ] T018 CORS and security headers + +## Phase 3.5: Polish +- [ ] T019 [P] Unit tests for validation in tests/unit/test_validation.py +- [ ] T020 Performance tests (<200ms) +- [ ] T021 [P] Update docs/api.md +- [ ] T022 Remove duplication +- [ ] T023 Run manual-testing.md + +## Dependencies +- Tests (T004-T007) before implementation (T008-T014) +- T008 blocks T009, T015 +- T016 blocks T018 +- Implementation before polish (T019-T023) + +## Parallel Example +``` +# Launch T004-T007 together: +Task: "Contract test POST /api/users in tests/contract/test_users_post.py" +Task: "Contract test GET /api/users/{id} in tests/contract/test_users_get.py" +Task: "Integration test registration in tests/integration/test_registration.py" +Task: "Integration test auth in tests/integration/test_auth.py" +``` + +## Notes +- [P] tasks = different files, no dependencies +- Verify tests fail before implementing +- Commit after each task +- Avoid: vague tasks, same file conflicts + +## Task Generation Rules +*Applied during main() execution* + +1. **From Contracts**: + - Each contract file → contract test task [P] + - Each endpoint → implementation task + +2. **From Data Model**: + - Each entity → model creation task [P] + - Relationships → service layer tasks + +3. **From User Stories**: + - Each story → integration test [P] + - Quickstart scenarios → validation tasks + +4. **Ordering**: + - Setup → Tests → Models → Services → Endpoints → Polish + - Dependencies block parallel execution + +## Validation Checklist +*GATE: Checked by main() before returning* + +- [ ] All contracts have corresponding tests +- [ ] All entities have model tasks +- [ ] All tests come before implementation +- [ ] Parallel tasks truly independent +- [ ] Each task specifies exact file path +- [ ] No task modifies same file as another [P] task \ No newline at end of file diff --git a/tests/ClientPortalTest.php b/tests/ClientPortalTest.php index d0a2f1d..85f286c 100644 --- a/tests/ClientPortalTest.php +++ b/tests/ClientPortalTest.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + '127.0.0.1', + 'desk_moloni_redis_port' => 6379, + 'desk_moloni_redis_password' => '', + 'desk_moloni_redis_db' => 1, + 'desk_moloni_sync_enabled' => '1', + 'desk_moloni_sync_customers' => '1', + 'desk_moloni_sync_invoices' => '1', + 'desk_moloni_sync_products' => '1', + 'desk_moloni_sync_estimates' => '1', + 'desk_moloni_sync_payments' => '1', + ]; + return $options[$name] ?? $default; + } + } + + // These services don't have complex dependencies in their constructors yet + $this->entity_mapping = new EntityMappingService(); $this->client_sync = new ClientSyncService(); $this->product_sync = new ProductSyncService(); $this->invoice_sync = new InvoiceSyncService(); $this->estimate_sync = new EstimateSyncService(); - $this->queue_processor = new QueueProcessor(); + + // Instantiate PerfexHooks, which will create the QueueProcessor with all its dependencies $this->perfex_hooks = new PerfexHooks(); - $this->entity_mapping = new EntityMappingService(); - - // Clear any existing test data + + // Get the QueueProcessor instance from the hooks class for test assertions + $reflection = new \ReflectionClass($this->perfex_hooks); + $queue_processor_property = $reflection->getProperty('queue_processor'); + $queue_processor_property->setAccessible(true); + $this->queue_processor = $queue_processor_property->getValue($this->perfex_hooks); + + // Clean up any existing test data $this->cleanupTestData(); } diff --git a/tests/QueueProcessorTest.php b/tests/QueueProcessorTest.php index a400e4c..d414247 100644 --- a/tests/QueueProcessorTest.php +++ b/tests/QueueProcessorTest.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ + redis_mock = $this->createMock(Redis::class); + $this->redis_mock = $this->createMock(\Redis::class); + $this->model_mock = $this->createMock(Desk_moloni_model::class); $this->entity_mapping_mock = $this->createMock(EntityMappingService::class); $this->error_handler_mock = $this->createMock(ErrorHandler::class); $this->retry_handler_mock = $this->createMock(RetryHandler::class); - + // Mock CodeIgniter instance $this->CI_mock = $this->createMock(stdClass::class); - $this->CI_mock->desk_moloni_model = $this->createMock(stdClass::class); - - // Initialize service - $this->queue_processor = new QueueProcessor(); - - // Use reflection to inject mocks - $reflection = new ReflectionClass($this->queue_processor); - - $redis_property = $reflection->getProperty('redis'); - $redis_property->setAccessible(true); - $redis_property->setValue($this->queue_processor, $this->redis_mock); - - $entity_mapping_property = $reflection->getProperty('entity_mapping'); - $entity_mapping_property->setAccessible(true); - $entity_mapping_property->setValue($this->queue_processor, $this->entity_mapping_mock); - - $error_handler_property = $reflection->getProperty('error_handler'); - $error_handler_property->setAccessible(true); - $error_handler_property->setValue($this->queue_processor, $this->error_handler_mock); - - $retry_handler_property = $reflection->getProperty('retry_handler'); - $retry_handler_property->setAccessible(true); - $retry_handler_property->setValue($this->queue_processor, $this->retry_handler_mock); - - $ci_property = $reflection->getProperty('CI'); - $ci_property->setAccessible(true); - $ci_property->setValue($this->queue_processor, $this->CI_mock); + $this->CI_mock->desk_moloni_model = $this->model_mock; + + // Initialize service with DI + $this->queue_processor = new QueueProcessor( + $this->redis_mock, + $this->model_mock, + $this->entity_mapping_mock, + $this->error_handler_mock, + $this->retry_handler_mock + ); } public function testAddToQueueSuccess() diff --git a/tests/TestCase.php b/tests/TestCase.php index cf5521d..62518dc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,3 +1,8 @@ +/** + * Descomplicar® Crescimento Digital + * https://descomplicar.pt + */ +