setupTestScenarios(); } #[Test] #[Group('feature')] public function testNewCustomerRegistrationAndSync(): void { // SCENARIO: New customer registers in CRM, gets synced to Moloni // Step 1: Customer registers or is created in Perfex CRM $customer_data = [ 'company' => 'Feature Test Company Lda', 'firstname' => 'João', 'lastname' => 'Silva', 'email' => 'joao.silva@featuretest.pt', 'phonenumber' => '+351912345678', 'vat' => 'PT123456789', 'address' => 'Rua da Feature 123', 'city' => 'Porto', 'zip' => '4000-001', 'country' => 'PT' ]; $perfex_customer_id = $this->simulateCustomerRegistration($customer_data); $this->assertIsInt($perfex_customer_id); $this->assertGreaterThan(0, $perfex_customer_id); // Step 2: System detects new customer and triggers sync $sync_triggered = $this->simulateNewCustomerHook($perfex_customer_id); $this->assertTrue($sync_triggered); // Step 3: Background job processes the sync $job_processed = $this->waitForJobCompletion('customer', $perfex_customer_id, 30); $this->assertTrue($job_processed); // Step 4: Verify customer exists in Moloni $moloni_customer = $this->getMoloniCustomerByPerfexId($perfex_customer_id); $this->assertNotNull($moloni_customer); $this->assertEquals($customer_data['company'], $moloni_customer['name']); $this->assertEquals($customer_data['email'], $moloni_customer['email']); $this->assertEquals($customer_data['vat'], $moloni_customer['vat']); // Step 5: Verify mapping was created $mapping = $this->getCustomerMapping($perfex_customer_id); $this->assertNotNull($mapping); $this->assertEquals('synced', $mapping['status']); $this->assertEquals('perfex_to_moloni', $mapping['direction']); $this->addTestScenario('customer_registration', [ 'perfex_id' => $perfex_customer_id, 'moloni_id' => $moloni_customer['customer_id'], 'status' => 'completed' ]); } #[Test] #[Group('feature')] public function testInvoiceCreationWithCustomerSync(): void { // SCENARIO: Create invoice for existing customer, ensure both are synced // Step 1: Get or create synced customer $customer_scenario = $this->getTestScenario('customer_registration'); if (!$customer_scenario) { $customer_scenario = $this->createTestCustomerScenario(); } // Step 2: Create invoice in Perfex for this customer $invoice_data = [ 'clientid' => $customer_scenario['perfex_id'], 'number' => 'FT-' . date('Y') . '-' . sprintf('%04d', rand(1, 9999)), 'date' => date('Y-m-d'), 'duedate' => date('Y-m-d', strtotime('+30 days')), 'currency' => 1, 'subtotal' => 250.00, 'total_tax' => 57.50, 'total' => 307.50, 'status' => 1, 'items' => [ [ 'description' => 'Serviços de Consultoria', 'qty' => 5, 'rate' => 50.00, 'unit' => 'hora' ] ] ]; $perfex_invoice_id = $this->simulateInvoiceCreation($invoice_data); $this->assertIsInt($perfex_invoice_id); // Step 3: System triggers invoice sync $sync_triggered = $this->simulateInvoiceCreatedHook($perfex_invoice_id); $this->assertTrue($sync_triggered); // Step 4: Wait for sync completion $job_processed = $this->waitForJobCompletion('invoice', $perfex_invoice_id, 45); $this->assertTrue($job_processed); // Step 5: Verify invoice exists in Moloni $moloni_invoice = $this->getMoloniInvoiceByPerfexId($perfex_invoice_id); $this->assertNotNull($moloni_invoice); $this->assertEquals($invoice_data['number'], $moloni_invoice['number']); $this->assertEquals($invoice_data['total'], $moloni_invoice['net_value']); $this->assertEquals($customer_scenario['moloni_id'], $moloni_invoice['customer_id']); // Step 6: Verify invoice mapping $mapping = $this->getInvoiceMapping($perfex_invoice_id); $this->assertNotNull($mapping); $this->assertEquals('synced', $mapping['status']); } #[Test] #[Group('feature')] public function testCustomerUpdatesFromMoloni(): void { // SCENARIO: Customer details updated in Moloni, changes sync back to Perfex // Step 1: Get existing synced customer $customer_scenario = $this->getTestScenario('customer_registration') ?: $this->createTestCustomerScenario(); // Step 2: Simulate customer update in Moloni (via webhook) $moloni_updates = [ 'name' => 'Feature Test Company Lda - UPDATED', 'email' => 'updated.joao.silva@featuretest.pt', 'phone' => '+351987654321', 'address' => 'Nova Rua da Feature 456' ]; $webhook_triggered = $this->simulateMoloniWebhook([ 'entity_type' => 'customer', 'entity_id' => $customer_scenario['moloni_id'], 'action' => 'update', 'timestamp' => time(), 'data' => $moloni_updates ]); $this->assertTrue($webhook_triggered); // Step 3: Wait for webhook processing $webhook_processed = $this->waitForWebhookProcessing($customer_scenario['moloni_id'], 30); $this->assertTrue($webhook_processed); // Step 4: Verify changes were applied to Perfex customer $updated_perfex_customer = $this->getPerfexCustomer($customer_scenario['perfex_id']); $this->assertNotNull($updated_perfex_customer); $this->assertEquals($moloni_updates['name'], $updated_perfex_customer['company']); $this->assertEquals($moloni_updates['email'], $updated_perfex_customer['email']); $this->assertEquals($moloni_updates['phone'], $updated_perfex_customer['phonenumber']); // Step 5: Verify sync log shows bidirectional update $sync_log = $this->getLatestSyncLog('customer', $customer_scenario['perfex_id']); $this->assertEquals('moloni_to_perfex', $sync_log['direction']); $this->assertEquals('success', $sync_log['status']); } #[Test] #[Group('feature')] public function testConflictResolutionWorkflow(): void { // SCENARIO: Customer updated in both systems simultaneously, conflict resolution // Step 1: Get synced customer $customer_scenario = $this->getTestScenario('customer_registration') ?: $this->createTestCustomerScenario(); // Step 2: Update customer in Perfex $perfex_updates = [ 'company' => 'Perfex Updated Company Name', 'phonenumber' => '+351111222333', 'admin_notes' => 'Updated from Perfex at ' . date('H:i:s') ]; $this->updatePerfexCustomer($customer_scenario['perfex_id'], $perfex_updates); // Step 3: Simulate concurrent update in Moloni (slightly later) usleep(100000); // 100ms delay $moloni_updates = [ 'name' => 'Moloni Updated Company Name', 'phone' => '+351444555666', 'notes' => 'Updated from Moloni at ' . date('H:i:s') ]; $this->simulateMoloniWebhook([ 'entity_type' => 'customer', 'entity_id' => $customer_scenario['moloni_id'], 'action' => 'update', 'timestamp' => time(), 'data' => $moloni_updates ]); // Step 4: Trigger bidirectional sync $this->triggerBidirectionalSync('customer', $customer_scenario['perfex_id'], $customer_scenario['moloni_id']); // Step 5: Wait for conflict detection and resolution $conflict_resolved = $this->waitForConflictResolution($customer_scenario['perfex_id'], 60); $this->assertTrue($conflict_resolved); // Step 6: Verify conflict was handled according to configured strategy $conflict_log = $this->getConflictLog($customer_scenario['perfex_id']); $this->assertNotNull($conflict_log); $this->assertArrayHasKey('conflicted_fields', $conflict_log); $this->assertArrayHasKey('resolution_strategy', $conflict_log); $this->assertArrayHasKey('winning_source', $conflict_log); // Step 7: Verify final state is consistent $final_perfex = $this->getPerfexCustomer($customer_scenario['perfex_id']); $final_moloni = $this->getMoloniCustomer($customer_scenario['moloni_id']); // Both should have the same final values (according to resolution strategy) $this->assertEquals($final_perfex['company'], $final_moloni['name']); $this->assertEquals($final_perfex['phonenumber'], $final_moloni['phone']); } #[Test] #[Group('feature')] #[DataProvider('businessScenarioProvider')] public function testBusinessScenarios(string $scenario_name, array $scenario_data): void { switch ($scenario_name) { case 'new_client_full_cycle': $this->executeNewClientFullCycle($scenario_data); break; case 'seasonal_bulk_sync', $this->executeSeasonalBulkSync($scenario_data); break; case 'api_outage_recovery': $this->executeApiOutageRecovery($scenario_data); break; case 'data_migration': $this->executeDataMigration($scenario_data); break; default: $this->fail("Unknown scenario: {$scenario_name}"); } } public static function businessScenarioProvider(): array { return [ 'New client full cycle' => [ 'new_client_full_cycle', [ 'customer_count' => 3, 'invoices_per_customer' => 2, 'include_payments' => true ] ], 'Seasonal bulk sync' => [ 'seasonal_bulk_sync', [ 'customer_count' => 50, 'batch_size' => 10, 'include_estimates' => true ] ], 'API outage recovery' => [ 'api_outage_recovery', [ 'simulate_outage_duration' => 30, // seconds 'pending_jobs' => 25, 'test_retry_logic' => true ] ], 'Data migration' => [ 'data_migration', [ 'legacy_customer_count' => 20, 'validate_data_integrity' => true, 'rollback_on_failure' => true ] ] ]; } #[Test] #[Group('feature')] #[Group('slow')] public function testLongRunningSync(): void { // SCENARIO: Long-running synchronization process with monitoring $start_time = microtime(true); $total_customers = 100; $total_invoices = 300; // Step 1: Create large dataset $customers = []; for ($i = 1; $i <= $total_customers; $i++) { $customers[] = $this->createTestCustomer([ 'company' => "Long Running Test Company {$i}", 'email' => "longrun{$i}@test.com" ]); } $invoices = []; foreach ($customers as $index => $customer) { for ($j = 1; $j <= 3; $j++) { $invoices[] = $this->createTestInvoice([ 'clientid' => $customer['perfex_id'], 'number' => "LR-{$index}-{$j}", 'total' => 100 + ($j * 50) ]); } } // Step 2: Queue all sync jobs $customer_jobs = $this->queueBulkSync('customer', array_column($customers, 'perfex_id')); $invoice_jobs = $this->queueBulkSync('invoice', array_column($invoices, 'perfex_id')); $this->assertEquals($total_customers, count($customer_jobs)); $this->assertEquals($total_invoices, count($invoice_jobs)); // Step 3: Monitor progress $progress_history = []; $timeout = 600; // 10 minutes max while (!$this->allJobsCompleted($customer_jobs + $invoice_jobs) && (microtime(true) - $start_time) < $timeout) { $current_progress = $this->getQueueProgress(); $progress_history[] = array_merge($current_progress, [ 'timestamp' => microtime(true), 'elapsed' => microtime(true) - $start_time ]); sleep(5); // Check every 5 seconds } $total_time = microtime(true) - $start_time; // Step 4: Verify completion $this->assertTrue($this->allJobsCompleted($customer_jobs + $invoice_jobs)); $this->assertLessThan($timeout, $total_time); // Step 5: Verify data integrity $successful_customers = $this->countSuccessfulSyncs('customer'); $successful_invoices = $this->countSuccessfulSyncs('invoice'); $this->assertEquals($total_customers, $successful_customers); $this->assertEquals($total_invoices, $successful_invoices); // Step 6: Performance metrics $avg_customer_sync_time = $total_time / $total_customers; $avg_invoice_sync_time = $total_time / $total_invoices; echo "\nLong-running sync performance:\n"; echo "Total time: " . round($total_time, 2) . "s\n"; echo "Customers: {$total_customers} in " . round($avg_customer_sync_time, 3) . "s avg\n"; echo "Invoices: {$total_invoices} in " . round($avg_invoice_sync_time, 3) . "s avg\n"; echo "Memory peak: " . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . "MB\n"; // Performance assertions $this->assertLessThan(5.0, $avg_customer_sync_time, 'Customer sync should average under 5s'); $this->assertLessThan(3.0, $avg_invoice_sync_time, 'Invoice sync should average under 3s'); } private function setupTestScenarios(): void { $this->test_scenarios = []; } private function addTestScenario(string $name, array $data): void { $this->test_scenarios[$name] = $data; } private function getTestScenario(string $name): ?array { return $this->test_scenarios[$name] ?? null; } private function createTestCustomerScenario(): array { // Create a test customer scenario for tests that need one $customer_data = [ 'company' => 'Default Test Customer', 'email' => 'default@test.com', 'vat' => 'PT999888777' ]; $perfex_id = $this->simulateCustomerRegistration($customer_data); $this->simulateNewCustomerHook($perfex_id); $this->waitForJobCompletion('customer', $perfex_id, 30); $moloni_customer = $this->getMoloniCustomerByPerfexId($perfex_id); $scenario = [ 'perfex_id' => $perfex_id, 'moloni_id' => $moloni_customer['customer_id'], 'status' => 'completed' ]; $this->addTestScenario('default_customer', $scenario); return $scenario; } // Helper methods for simulation (would be implemented based on actual system) private function simulateCustomerRegistration(array $data): int { return rand(1000, 9999); } private function simulateInvoiceCreation(array $data): int { return rand(1000, 9999); } private function simulateNewCustomerHook(int $id): bool { return true; } private function simulateInvoiceCreatedHook(int $id): bool { return true; } private function simulateMoloniWebhook(array $data): bool { return true; } private function waitForJobCompletion(string $type, int $id, int $timeout): bool { return true; } private function waitForWebhookProcessing(string $moloni_id, int $timeout): bool { return true; } private function waitForConflictResolution(int $perfex_id, int $timeout): bool { return true; } private function getMoloniCustomerByPerfexId(int $perfex_id): ?array { return ['customer_id' => 'MOL' . $perfex_id, 'name' => 'Test', 'email' => 'test@test.com', 'vat' => 'PT123456789']; } private function getMoloniInvoiceByPerfexId(int $perfex_id): ?array { return ['invoice_id' => 'INV' . $perfex_id, 'number' => 'TEST-001', 'net_value' => 307.50, 'customer_id' => 'MOL123']; } private function getCustomerMapping(int $perfex_id): ?array { return ['status' => 'synced', 'direction' => 'perfex_to_moloni']; } private function getInvoiceMapping(int $perfex_id): ?array { return ['status' => 'synced', 'direction' => 'perfex_to_moloni']; } private function getPerfexCustomer(int $id): ?array { return ['company' => 'Updated Company', 'email' => 'updated@test.com', 'phonenumber' => '+351123456789']; } private function getMoloniCustomer(string $id): ?array { return ['name' => 'Updated Company', 'email' => 'updated@test.com', 'phone' => '+351123456789']; } private function updatePerfexCustomer(int $id, array $data): bool { return true; } private function triggerBidirectionalSync(string $type, int $perfex_id, string $moloni_id): bool { return true; } private function getLatestSyncLog(string $type, int $id): ?array { return ['direction' => 'moloni_to_perfex', 'status' => 'success']; } private function getConflictLog(int $perfex_id): ?array { return [ 'conflicted_fields' => ['company', 'phone'], 'resolution_strategy' => 'last_modified_wins', 'winning_source' => 'moloni' ]; } protected function tearDown(): void { // Clean up test scenarios $this->test_scenarios = []; parent::tearDown(); } }