/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ testConfig = $testConfig; $this->pdo = new \PDO( "mysql:host={$testConfig['database']['hostname']};dbname={$testConfig['database']['database']}", $testConfig['database']['username'], $testConfig['database']['password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION] ); // Clean test data TestHelpers::clearTestData(); } /** * Test Perfex to Moloni client synchronization workflow * This test will initially fail until sync engine implementation exists */ public function testPerfexToMoloniClientSync(): void { // Create test client in Perfex format $perfexClient = TestHelpers::createTestClient([ 'userid' => 9999, 'company' => 'Test Company Integration', 'vat' => '999999990', 'phonenumber' => '+351910000001', 'country' => 191, // Portugal 'city' => 'Porto', 'address' => 'Rua de Teste, 123', 'zip' => '4000-001', 'billing_street' => 'Rua de Faturação, 456', 'billing_city' => 'Porto', 'billing_zip' => '4000-002' ]); // This should trigger sync process (will fail initially) $syncService = new \DeskMoloni\ClientSyncService(); $result = $syncService->syncPerfexToMoloni($perfexClient); // Validate sync result structure $this->assertIsArray($result); $this->assertArrayHasKey('success', $result); $this->assertArrayHasKey('moloni_id', $result); $this->assertArrayHasKey('mapping_id', $result); if ($result['success']) { $this->assertIsInt($result['moloni_id']); $this->assertGreaterThan(0, $result['moloni_id']); $this->assertIsInt($result['mapping_id']); // Verify mapping was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_mapping WHERE entity_type = 'client' AND perfex_id = ?"); $stmt->execute([$perfexClient['userid']]); $mapping = $stmt->fetch(); $this->assertNotFalse($mapping, 'Client mapping should be created'); $this->assertEquals($result['moloni_id'], $mapping['moloni_id']); $this->assertEquals('perfex_to_moloni', $mapping['sync_direction']); $this->assertNotNull($mapping['last_sync_at']); // Verify sync log was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND perfex_id = ? AND direction = 'perfex_to_moloni'"); $stmt->execute([$perfexClient['userid']]); $log = $stmt->fetch(); $this->assertNotFalse($log, 'Sync log should be created'); $this->assertEquals('success', $log['status']); $this->assertEquals('create', $log['operation_type']); $this->assertNotNull($log['request_data']); $this->assertNotNull($log['response_data']); } else { // If sync failed, verify error is logged $this->assertArrayHasKey('error', $result); $this->assertIsString($result['error']); $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND perfex_id = ? AND status = 'error'"); $stmt->execute([$perfexClient['userid']]); $log = $stmt->fetch(); $this->assertNotFalse($log, 'Error log should be created'); } } /** * Test Moloni to Perfex client synchronization workflow */ public function testMoloniToPerfexClientSync(): void { // Create test client in Moloni format (simulated API response) $moloniClient = [ 'customer_id' => 8888, 'vat' => '999999991', 'number' => 'CLI-2025-001', 'name' => 'Moloni Test Company', 'email' => 'moloni-test@example.com', 'phone' => '+351910000002', 'address' => 'Avenida da República, 789', 'zip_code' => '1000-001', 'city' => 'Lisboa', 'country_id' => 1 ]; // This should trigger reverse sync process (will fail initially) $syncService = new \DeskMoloni\ClientSyncService(); $result = $syncService->syncMoloniToPerfex($moloniClient); // Validate sync result structure $this->assertIsArray($result); $this->assertArrayHasKey('success', $result); $this->assertArrayHasKey('perfex_id', $result); $this->assertArrayHasKey('mapping_id', $result); if ($result['success']) { $this->assertIsInt($result['perfex_id']); $this->assertGreaterThan(0, $result['perfex_id']); // Verify mapping was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_mapping WHERE entity_type = 'client' AND moloni_id = ?"); $stmt->execute([$moloniClient['customer_id']]); $mapping = $stmt->fetch(); $this->assertNotFalse($mapping, 'Client mapping should be created'); $this->assertEquals($result['perfex_id'], $mapping['perfex_id']); $this->assertEquals('moloni_to_perfex', $mapping['sync_direction']); // Verify sync log was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND moloni_id = ? AND direction = 'moloni_to_perfex'"); $stmt->execute([$moloniClient['customer_id']]); $log = $stmt->fetch(); $this->assertNotFalse($log, 'Sync log should be created'); $this->assertEquals('success', $log['status']); } } /** * Test bidirectional client synchronization conflict resolution */ public function testBidirectionalSyncConflictResolution(): void { // Create existing mapping for bidirectional sync $stmt = $this->pdo->prepare("INSERT INTO tbl_desk_moloni_mapping (entity_type, perfex_id, moloni_id, sync_direction, last_sync_at) VALUES (?, ?, ?, ?, ?)"); $stmt->execute(['client', 7777, 6666, 'bidirectional', date('Y-m-d H:i:s', strtotime('-1 hour'))]); $mappingId = $this->pdo->lastInsertId(); // Simulate concurrent updates on both systems $perfexUpdate = [ 'userid' => 7777, 'company' => 'Updated Company Name Perfex', 'phonenumber' => '+351910000003', 'address' => 'Updated Perfex Address' ]; $moloniUpdate = [ 'customer_id' => 6666, 'name' => 'Updated Company Name Moloni', 'phone' => '+351910000004', 'address' => 'Updated Moloni Address' ]; // Test conflict detection and resolution $syncService = new \DeskMoloni\ClientSyncService(); $conflictResult = $syncService->resolveConflict($perfexUpdate, $moloniUpdate, $mappingId); $this->assertIsArray($conflictResult); $this->assertArrayHasKey('conflict_detected', $conflictResult); $this->assertArrayHasKey('resolution_strategy', $conflictResult); $this->assertArrayHasKey('merged_data', $conflictResult); if ($conflictResult['conflict_detected']) { $this->assertContains($conflictResult['resolution_strategy'], [ 'perfex_wins', 'moloni_wins', 'manual_merge', 'timestamp_based' ]); // Verify conflict log is created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND operation_type = 'update' AND status = 'warning'"); $stmt->execute(); $logs = $stmt->fetchAll(); $this->assertNotEmpty($logs, 'Conflict should be logged as warning'); } } /** * Test client sync with field mapping and validation */ public function testClientSyncWithFieldMapping(): void { $perfexClient = TestHelpers::createTestClient([ 'userid' => 5555, 'company' => 'Field Mapping Test Company', 'vat' => '999999995', 'phonenumber' => '+351910000005', 'website' => 'https://test-company.com', 'custom_fields' => json_encode([ 'cf_1' => 'Custom Value 1', 'cf_2' => 'Custom Value 2' ]) ]); // Test field mapping and validation $syncService = new \DeskMoloni\ClientSyncService(); $mappingResult = $syncService->mapPerfexToMoloniFields($perfexClient); $this->assertIsArray($mappingResult); $this->assertArrayHasKey('mapped_fields', $mappingResult); $this->assertArrayHasKey('validation_errors', $mappingResult); $this->assertArrayHasKey('unmapped_fields', $mappingResult); $mappedFields = $mappingResult['mapped_fields']; // Validate required field mappings $this->assertArrayHasKey('vat', $mappedFields); $this->assertArrayHasKey('name', $mappedFields); $this->assertArrayHasKey('phone', $mappedFields); // Validate field transformations $this->assertEquals($perfexClient['company'], $mappedFields['name']); $this->assertEquals($perfexClient['vat'], $mappedFields['vat']); $this->assertEquals($perfexClient['phonenumber'], $mappedFields['phone']); // Test validation rules if (!empty($mappingResult['validation_errors'])) { $this->assertIsArray($mappingResult['validation_errors']); foreach ($mappingResult['validation_errors'] as $error) { $this->assertArrayHasKey('field', $error); $this->assertArrayHasKey('message', $error); $this->assertArrayHasKey('value', $error); } } } /** * Test client sync error handling and retry mechanism */ public function testClientSyncErrorHandlingAndRetry(): void { // Create invalid client data to trigger API error $invalidClient = [ 'userid' => 3333, 'company' => '', // Empty required field 'vat' => 'INVALID_VAT', 'phonenumber' => 'INVALID_PHONE' ]; $syncService = new \DeskMoloni\ClientSyncService(); $result = $syncService->syncPerfexToMoloni($invalidClient); $this->assertIsArray($result); $this->assertFalse($result['success']); $this->assertArrayHasKey('error', $result); $this->assertArrayHasKey('error_code', $result); $this->assertArrayHasKey('retry_count', $result); // Verify error is logged with proper categorization $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND perfex_id = ? AND status = 'error'"); $stmt->execute([$invalidClient['userid']]); $log = $stmt->fetch(); $this->assertNotFalse($log, 'Error should be logged'); $this->assertNotNull($log['error_code']); $this->assertNotNull($log['error_message']); $this->assertNotNull($log['request_data']); // Test retry mechanism if ($result['retry_count'] > 0) { $retryResult = $syncService->retrySyncOperation($log['id']); $this->assertIsArray($retryResult); $this->assertArrayHasKey('retry_attempted', $retryResult); } } /** * Test client sync performance and queue integration */ public function testClientSyncPerformanceAndQueue(): void { $startTime = microtime(true); // Create multiple clients for batch sync testing $testClients = []; for ($i = 1; $i <= 5; $i++) { $testClients[] = TestHelpers::createTestClient([ 'userid' => 2000 + $i, 'company' => "Batch Test Company {$i}", 'vat' => "99999999{$i}", 'phonenumber' => "+35191000000{$i}" ]); } // Test batch sync performance $syncService = new \DeskMoloni\ClientSyncService(); $batchResult = $syncService->batchSyncPerfexToMoloni($testClients); $endTime = microtime(true); $executionTime = ($endTime - $startTime) * 1000; // Convert to milliseconds $this->assertIsArray($batchResult); $this->assertArrayHasKey('total_processed', $batchResult); $this->assertArrayHasKey('successful_syncs', $batchResult); $this->assertArrayHasKey('failed_syncs', $batchResult); $this->assertArrayHasKey('execution_time_ms', $batchResult); // Performance assertions $this->assertLessThan(30000, $executionTime, 'Batch sync should complete within 30 seconds'); $this->assertEquals(count($testClients), $batchResult['total_processed']); // Verify queue tasks were created $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE task_type = 'sync_client' AND entity_type = 'client'"); $stmt->execute(); $queueCount = $stmt->fetch(); $this->assertGreaterThan(0, $queueCount['count'], 'Queue tasks should be created for batch sync'); // Verify performance metrics are logged $stmt = $this->pdo->prepare("SELECT AVG(execution_time_ms) as avg_time FROM tbl_desk_moloni_sync_log WHERE entity_type = 'client' AND execution_time_ms IS NOT NULL"); $stmt->execute(); $avgTime = $stmt->fetch(); if ($avgTime['avg_time'] !== null) { $this->assertLessThan(5000, $avgTime['avg_time'], 'Average sync time should be under 5 seconds'); } } /** * Test client sync webhook processing */ public function testClientSyncWebhookProcessing(): void { // Simulate Moloni webhook payload for customer update $webhookPayload = [ 'webhook_id' => 'webhook_' . time(), 'event_type' => 'customer.updated', 'entity_type' => 'client', 'entity_id' => 4444, 'event_data' => [ 'customer_id' => 4444, 'name' => 'Webhook Updated Company', 'email' => 'webhook-updated@example.com', 'updated_at' => date('Y-m-d H:i:s') ] ]; $syncService = new \DeskMoloni\ClientSyncService(); $webhookResult = $syncService->processWebhook($webhookPayload); $this->assertIsArray($webhookResult); $this->assertArrayHasKey('processed', $webhookResult); $this->assertArrayHasKey('queue_task_created', $webhookResult); if ($webhookResult['processed']) { // Verify webhook record was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_webhooks WHERE webhook_id = ?"); $stmt->execute([$webhookPayload['webhook_id']]); $webhook = $stmt->fetch(); $this->assertNotFalse($webhook, 'Webhook should be recorded'); $this->assertEquals(1, $webhook['processed']); $this->assertNotNull($webhook['processed_at']); if ($webhookResult['queue_task_created']) { // Verify queue task was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_queue WHERE task_type = 'sync_client' AND entity_id = ?"); $stmt->execute([$webhookPayload['entity_id']]); $queueTask = $stmt->fetch(); $this->assertNotFalse($queueTask, 'Queue task should be created from webhook'); $this->assertEquals('pending', $queueTask['status']); } } } protected function tearDown(): void { // Clean up test data $testIds = [9999, 8888, 7777, 6666, 5555, 4444, 3333]; $testIds = array_merge($testIds, range(2001, 2005)); foreach ($testIds as $id) { $this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id = {$id} OR moloni_id = {$id}"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id = {$id} OR moloni_id = {$id}"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_queue WHERE entity_id = {$id}"); } $this->pdo->exec("DELETE FROM tbl_desk_moloni_webhooks WHERE webhook_id LIKE 'webhook_%'"); } }