client_sync_service = new ClientSyncService(); $this->invoice_sync_service = new InvoiceSyncService(); $this->queue_processor = new QueueProcessor(); $this->api_client = new MoloniApiClient(); $this->mapping_service = new EntityMappingService(); // Initialize test environment $this->setupTestEnvironment(); } #[Test] #[Group('integration')] public function testCompleteCustomerToInvoiceWorkflow(): void { // Step 1: Create customer in Perfex $customer_data = $this->createTestCustomer([ 'company' => 'Integration Test Customer Ltd', 'email' => 'integration@testcustomer.com', 'vat' => 'PT123456789' ]); $this->assertNotNull($customer_data['perfex_id']); // Step 2: Sync customer to Moloni $customer_sync_result = $this->client_sync_service->sync_client_to_moloni($customer_data['perfex_id']); $this->assertTrue($customer_sync_result['success']); $this->assertNotNull($customer_sync_result['moloni_id']); // Step 3: Create invoice in Perfex for this customer $invoice_data = $this->createTestInvoice([ 'clientid' => $customer_data['perfex_id'], 'number' => 'INT-TEST-' . date('Ymd-His'), 'subtotal' => 100.00, 'total_tax' => 23.00, 'total' => 123.00 ]); $this->assertNotNull($invoice_data['perfex_id']); // Step 4: Sync invoice to Moloni $invoice_sync_result = $this->invoice_sync_service->sync_invoice_to_moloni($invoice_data['perfex_id']); $this->assertTrue($invoice_sync_result['success']); $this->assertNotNull($invoice_sync_result['moloni_id']); // Step 5: Verify mappings were created $customer_mapping = $this->mapping_service->get_mapping_by_perfex_id('customer', $customer_data['perfex_id']); $invoice_mapping = $this->mapping_service->get_mapping_by_perfex_id('invoice', $invoice_data['perfex_id']); $this->assertNotNull($customer_mapping); $this->assertNotNull($invoice_mapping); $this->assertEquals('synced', $customer_mapping->sync_status); $this->assertEquals('synced', $invoice_mapping->sync_status); // Step 6: Verify data consistency $this->verifyDataConsistency($customer_data['perfex_id'], $customer_sync_result['moloni_id'], 'customer'); $this->verifyDataConsistency($invoice_data['perfex_id'], $invoice_sync_result['moloni_id'], 'invoice'); } #[Test] #[Group('integration')] public function testQueueBasedSynchronization(): void { // Create multiple entities for queue processing $customers = []; for ($i = 1; $i <= 5; $i++) { $customers[] = $this->createTestCustomer([ 'company' => "Queue Test Customer {$i}", 'email' => "queue{$i}@test.com", 'vat' => "PT12345678{$i}" ]); } // Add all customers to sync queue $job_ids = []; foreach ($customers as $customer) { $job_id = $this->queue_processor->add_to_queue( 'customer', $customer['perfex_id'], 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL ); $this->assertNotFalse($job_id); $job_ids[] = $job_id; } // Process queue $process_result = $this->queue_processor->process_queue(count($customers), 300); $this->assertEquals(count($customers), $process_result['processed']); $this->assertEquals(count($customers), $process_result['success']); $this->assertEquals(0, $process_result['errors']); // Verify all customers were synced foreach ($customers as $customer) { $mapping = $this->mapping_service->get_mapping_by_perfex_id('customer', $customer['perfex_id']); $this->assertNotNull($mapping); $this->assertEquals('synced', $mapping->sync_status); } } #[Test] #[Group('integration')] public function testBidirectionalSyncWithConflicts(): void { // Create customer and sync initially $customer_data = $this->createTestCustomer([ 'company' => 'Bidirectional Test Company', 'email' => 'bidirectional@test.com' ]); $initial_sync = $this->client_sync_service->sync_client_to_moloni($customer_data['perfex_id']); $this->assertTrue($initial_sync['success']); // Simulate concurrent updates // Update in Perfex $this->updatePerfexCustomer($customer_data['perfex_id'], [ 'company' => 'Updated by Perfex System', 'phonenumber' => '+351999111222' ]); // Simulate update in Moloni (mock the API response) $this->simulateMoloniCustomerUpdate($initial_sync['moloni_id'], [ 'name' => 'Updated by Moloni System', 'phone' => '+351888333444' ]); // Trigger bidirectional sync $bidirectional_result = $this->client_sync_service->bidirectional_sync( $customer_data['perfex_id'], $initial_sync['moloni_id'] ); // Should detect conflicts $this->assertArrayHasKey('conflicts_detected', $bidirectional_result); if ($bidirectional_result['conflicts_detected']) { $this->assertArrayHasKey('conflicted_fields', $bidirectional_result); $this->assertContains('company', $bidirectional_result['conflicted_fields']); $this->assertContains('phone', $bidirectional_result['conflicted_fields']); } } #[Test] #[Group('integration')] public function testWebhookTriggeredSync(): void { // Create customer and sync to establish mapping $customer_data = $this->createTestCustomer([ 'company' => 'Webhook Test Company', 'email' => 'webhook@test.com' ]); $sync_result = $this->client_sync_service->sync_client_to_moloni($customer_data['perfex_id']); $this->assertTrue($sync_result['success']); // Simulate webhook from Moloni $webhook_payload = [ 'entity_type' => 'customer', 'entity_id' => $sync_result['moloni_id'], 'action' => 'update', 'event_type' => 'customer.updated', 'timestamp' => time(), 'data' => [ 'customer_id' => $sync_result['moloni_id'], 'name' => 'Updated via Webhook', 'email' => 'updated.webhook@test.com' ] ]; // Process webhook (would be handled by WebhookController in real scenario) $webhook_result = $this->processWebhookPayload($webhook_payload); $this->assertTrue($webhook_result['success']); $this->assertArrayHasKey('job_id', $webhook_result); // Process the queued job $process_result = $this->queue_processor->process_queue(1, 60); $this->assertEquals(1, $process_result['processed']); $this->assertEquals(1, $process_result['success']); // Verify customer was updated in Perfex $updated_customer = $this->getPerfexCustomer($customer_data['perfex_id']); $this->assertEquals('Updated via Webhook', $updated_customer['company']); $this->assertEquals('updated.webhook@test.com', $updated_customer['email']); } #[Test] #[Group('integration')] public function testErrorHandlingAndRecovery(): void { // Create customer with invalid data to trigger errors $customer_data = $this->createTestCustomer([ 'company' => 'Error Test Company', 'email' => 'invalid-email-format', // Invalid email 'vat' => 'INVALID_VAT' // Invalid VAT ]); // First sync attempt should fail with validation errors $sync_result = $this->client_sync_service->sync_client_to_moloni($customer_data['perfex_id']); $this->assertFalse($sync_result['success']); $this->assertArrayHasKey('errors', $sync_result); // Fix the customer data $this->updatePerfexCustomer($customer_data['perfex_id'], [ 'email' => 'corrected@email.com', 'vat' => 'PT123456789' ]); // Retry sync should now succeed $retry_result = $this->client_sync_service->sync_client_to_moloni($customer_data['perfex_id']); $this->assertTrue($retry_result['success']); $this->assertNotNull($retry_result['moloni_id']); // Verify mapping was created $mapping = $this->mapping_service->get_mapping_by_perfex_id('customer', $customer_data['perfex_id']); $this->assertNotNull($mapping); $this->assertEquals('synced', $mapping->sync_status); } #[Test] #[Group('integration')] #[DataProvider('massDataProvider')] public function testMassDataSynchronization(int $customer_count, int $invoice_count): void { $start_time = microtime(true); // Create customers $customers = []; for ($i = 1; $i <= $customer_count; $i++) { $customers[] = $this->createTestCustomer([ 'company' => "Mass Test Customer {$i}", 'email' => "mass{$i}@test.com" ]); } // Sync all customers using batch processing $customer_ids = array_column($customers, 'perfex_id'); $batch_result = $this->client_sync_service->batch_sync_clients_to_moloni($customer_ids); $this->assertEquals($customer_count, $batch_result['total']); $this->assertEquals($customer_count, $batch_result['success_count']); // Create invoices for each customer $invoices = []; foreach ($customers as $index => $customer) { for ($j = 1; $j <= $invoice_count; $j++) { $invoices[] = $this->createTestInvoice([ 'clientid' => $customer['perfex_id'], 'number' => "MASS-{$index}-{$j}-" . date('His'), 'subtotal' => 50.00 * $j, 'total' => 61.50 * $j // With 23% tax ]); } } // Sync all invoices $invoice_ids = array_column($invoices, 'perfex_id'); $invoice_batch_result = $this->invoice_sync_service->batch_sync_invoices_to_moloni($invoice_ids); $total_invoices = $customer_count * $invoice_count; $this->assertEquals($total_invoices, $invoice_batch_result['total']); $execution_time = microtime(true) - $start_time; // Performance assertions $this->assertLessThan(300, $execution_time, 'Mass sync should complete within 5 minutes'); // Memory usage should be reasonable $memory_mb = memory_get_peak_usage(true) / (1024 * 1024); $this->assertLessThan(256, $memory_mb, 'Memory usage should be under 256MB'); echo "\nMass sync performance: {$customer_count} customers + " . "{$total_invoices} invoices in " . round($execution_time, 2) . "s using " . round($memory_mb, 2) . "MB\n"; } public static function massDataProvider(): array { return [ 'Small batch' => [5, 2], // 5 customers, 2 invoices each = 10 invoices 'Medium batch' => [10, 3], // 10 customers, 3 invoices each = 30 invoices 'Large batch' => [20, 5] // 20 customers, 5 invoices each = 100 invoices ]; } #[Test] #[Group('integration')] public function testConcurrentSyncOperations(): void { if (!extension_loaded('pcntl')) { $this->markTestSkipped('pcntl extension not available for concurrent testing'); } $customers = []; for ($i = 1; $i <= 6; $i++) { $customers[] = $this->createTestCustomer([ 'company' => "Concurrent Test Customer {$i}", 'email' => "concurrent{$i}@test.com" ]); } // Split customers into groups for concurrent processing $group1 = array_slice($customers, 0, 3); $group2 = array_slice($customers, 3, 3); $pids = []; // Fork for group 1 $pid1 = pcntl_fork(); if ($pid1 == 0) { // Child process 1 foreach ($group1 as $customer) { $result = $this->client_sync_service->sync_client_to_moloni($customer['perfex_id']); if (!$result['success']) { exit(1); } } exit(0); } else { $pids[] = $pid1; } // Fork for group 2 $pid2 = pcntl_fork(); if ($pid2 == 0) { // Child process 2 foreach ($group2 as $customer) { $result = $this->client_sync_service->sync_client_to_moloni($customer['perfex_id']); if (!$result['success']) { exit(1); } } exit(0); } else { $pids[] = $pid2; } // Wait for all processes to complete $all_success = true; foreach ($pids as $pid) { $status = 0; pcntl_waitpid($pid, $status); if (pcntl_wexitstatus($status) !== 0) { $all_success = false; } } $this->assertTrue($all_success, 'All concurrent sync operations should succeed'); // Verify all customers were synced foreach ($customers as $customer) { $mapping = $this->mapping_service->get_mapping_by_perfex_id('customer', $customer['perfex_id']); $this->assertNotNull($mapping); $this->assertEquals('synced', $mapping->sync_status); } } private function setupTestEnvironment(): void { // Clean up any previous test data $this->cleanupTestData(); // Initialize test configuration $test_config = [ 'sync_enabled' => true, 'batch_size' => 10, 'api_timeout' => 30, 'max_retries' => 3 ]; foreach ($test_config as $key => $value) { // Set test configuration (would use config model in real implementation) } } private function createTestCustomer(array $data): array { // Mock customer creation in Perfex $perfex_id = rand(10000, 99999); // Store test customer data $this->test_customers[] = array_merge($data, ['perfex_id' => $perfex_id]); return ['perfex_id' => $perfex_id]; } private function createTestInvoice(array $data): array { // Mock invoice creation in Perfex $perfex_id = rand(10000, 99999); // Store test invoice data $this->test_invoices[] = array_merge($data, ['perfex_id' => $perfex_id]); return ['perfex_id' => $perfex_id]; } private function verifyDataConsistency(int $perfex_id, string $moloni_id, string $entity_type): void { // This would compare data between Perfex and Moloni to ensure consistency // For now, we'll just verify that both IDs exist and mapping is correct $mapping = $this->mapping_service->get_mapping_by_perfex_id($entity_type, $perfex_id); $this->assertNotNull($mapping); $this->assertEquals($moloni_id, $mapping->moloni_id); $this->assertEquals('synced', $mapping->sync_status); } private $test_customers = []; private $test_invoices = []; protected function tearDown(): void { // Clean up test data $this->cleanupTestData(); $this->client_sync_service = null; $this->invoice_sync_service = null; $this->queue_processor = null; $this->api_client = null; $this->mapping_service = null; parent::tearDown(); } private function cleanupTestData(): void { // Clean up test customers and invoices // In real implementation, this would clean up database records $this->test_customers = []; $this->test_invoices = []; } }