client_sync = new ClientSyncService(); $this->entity_mapping = new EntityMappingService(); // Mock API client $this->api_client_mock = $this->createMock(MoloniApiClient::class); // Set up test data $this->setupTestData(); // Inject mocked API client $reflection = new ReflectionClass($this->client_sync); $property = $reflection->getProperty('api_client'); $property->setAccessible(true); $property->setValue($this->client_sync, $this->api_client_mock); } protected function setupTestData() { $this->test_client_data = [ '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_state' => 'Lisboa', 'billing_zip' => '1000-001', 'billing_country' => 'PT', 'admin_notes' => 'Test client for integration testing' ]; $this->test_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' ]; } /** * Test complete Perfex to Moloni sync workflow */ public function test_complete_perfex_to_moloni_sync_workflow() { // Arrange $perfex_client_id = $this->test_client_data['userid']; // Mock Perfex client retrieval $this->mockPerfexClientRetrieval($perfex_client_id, $this->test_client_data); // Mock successful Moloni API creation $this->api_client_mock->expects($this->once()) ->method('create_customer') ->willReturn([ 'success' => true, 'data' => [ 'customer_id' => $this->test_moloni_data['customer_id'] ] ]); // Act $result = $this->client_sync->sync_perfex_to_moloni($perfex_client_id); // Assert $this->assertTrue($result['success']); $this->assertEquals('create', $result['action']); $this->assertEquals($this->test_moloni_data['customer_id'], $result['moloni_customer_id']); $this->assertIsInt($result['mapping_id']); $this->assertGreaterThan(0, $result['execution_time']); // Verify mapping was created $mapping = $this->entity_mapping->get_mapping_by_perfex_id( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id ); $this->assertNotNull($mapping); $this->assertEquals($perfex_client_id, $mapping->perfex_id); $this->assertEquals($this->test_moloni_data['customer_id'], $mapping->moloni_id); $this->assertEquals(EntityMappingService::STATUS_SYNCED, $mapping->sync_status); } /** * Test complete Moloni to Perfex sync workflow */ public function test_complete_moloni_to_perfex_sync_workflow() { // Arrange $moloni_customer_id = $this->test_moloni_data['customer_id']; // Mock Moloni API response $this->api_client_mock->expects($this->once()) ->method('get_customer') ->with($moloni_customer_id) ->willReturn([ 'success' => true, 'data' => $this->test_moloni_data ]); // Mock Perfex client creation $this->mockPerfexClientCreation($this->test_client_data['userid']); // Act $result = $this->client_sync->sync_moloni_to_perfex($moloni_customer_id); // Assert $this->assertTrue($result['success']); $this->assertEquals('create', $result['action']); $this->assertEquals($this->test_client_data['userid'], $result['perfex_client_id']); $this->assertIsInt($result['mapping_id']); // Verify mapping was created $mapping = $this->entity_mapping->get_mapping_by_moloni_id( EntityMappingService::ENTITY_CUSTOMER, $moloni_customer_id ); $this->assertNotNull($mapping); $this->assertEquals($this->test_client_data['userid'], $mapping->perfex_id); $this->assertEquals($moloni_customer_id, $mapping->moloni_id); $this->assertEquals(EntityMappingService::STATUS_SYNCED, $mapping->sync_status); } /** * Test sync with existing mapping (update scenario) */ public function test_sync_with_existing_mapping_update() { // Arrange - Create existing mapping $perfex_client_id = $this->test_client_data['userid']; $moloni_customer_id = $this->test_moloni_data['customer_id']; $mapping_id = $this->entity_mapping->create_mapping( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id, $moloni_customer_id, EntityMappingService::DIRECTION_PERFEX_TO_MOLONI ); // Mock Perfex client retrieval $this->mockPerfexClientRetrieval($perfex_client_id, $this->test_client_data); // Mock successful Moloni API update $this->api_client_mock->expects($this->once()) ->method('update_customer') ->with($moloni_customer_id) ->willReturn([ 'success' => true, 'data' => $this->test_moloni_data ]); // Act $result = $this->client_sync->sync_perfex_to_moloni($perfex_client_id, true); // Force update // Assert $this->assertTrue($result['success']); $this->assertEquals('update', $result['action']); $this->assertEquals($mapping_id, $result['mapping_id']); } /** * Test conflict detection and handling */ public function test_conflict_detection_and_handling() { // Arrange - Create mapping with conflicting data $perfex_client_id = $this->test_client_data['userid']; $moloni_customer_id = $this->test_moloni_data['customer_id']; $mapping_id = $this->entity_mapping->create_mapping( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id, $moloni_customer_id, EntityMappingService::DIRECTION_BIDIRECTIONAL ); // Set last sync time in the past $this->entity_mapping->update_mapping($mapping_id, [ 'last_sync_perfex' => date('Y-m-d H:i:s', strtotime('-1 hour')), 'last_sync_moloni' => date('Y-m-d H:i:s', strtotime('-1 hour')) ]); // Mock Perfex client with recent changes $modified_client_data = $this->test_client_data; $modified_client_data['company'] = 'Modified Company Name'; $this->mockPerfexClientRetrieval($perfex_client_id, $modified_client_data); // Mock Moloni customer with different recent changes $modified_moloni_data = $this->test_moloni_data; $modified_moloni_data['name'] = 'Different Modified Name'; $this->api_client_mock->expects($this->once()) ->method('get_customer') ->with($moloni_customer_id) ->willReturn([ 'success' => true, 'data' => $modified_moloni_data ]); // Mock modification time methods $this->mockModificationTimes(); // Act $result = $this->client_sync->sync_perfex_to_moloni($perfex_client_id); // Assert conflict detected $this->assertFalse($result['success']); $this->assertArrayHasKey('conflict_details', $result); $this->assertTrue($result['requires_manual_resolution']); // Verify mapping status updated to conflict $updated_mapping = $this->entity_mapping->get_mapping( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id, $moloni_customer_id ); $this->assertEquals(EntityMappingService::STATUS_CONFLICT, $updated_mapping->sync_status); } /** * Test error handling for API failures */ public function test_error_handling_for_api_failures() { // Arrange $perfex_client_id = $this->test_client_data['userid']; $this->mockPerfexClientRetrieval($perfex_client_id, $this->test_client_data); // Mock API failure $this->api_client_mock->expects($this->once()) ->method('create_customer') ->willReturn([ 'success' => false, 'message' => 'API authentication failed' ]); // Act $result = $this->client_sync->sync_perfex_to_moloni($perfex_client_id); // Assert $this->assertFalse($result['success']); $this->assertStringContains('Moloni API error', $result['message']); $this->assertGreaterThan(0, $result['execution_time']); } /** * Test customer matching functionality */ public function test_customer_matching_functionality() { // Arrange $search_data = [ 'company' => 'Test Company Ltd', 'vat' => 'PT123456789', 'email' => 'test@testcompany.com' ]; // Mock API search responses $this->api_client_mock->expects($this->once()) ->method('search_customers') ->with(['vat' => $search_data['vat']]) ->willReturn([ 'success' => true, 'data' => [ [ 'customer_id' => 888, 'name' => $search_data['company'], 'vat' => $search_data['vat'] ] ] ]); // Act $matches = $this->client_sync->find_moloni_customer_matches($search_data); // Assert $this->assertIsArray($matches); $this->assertNotEmpty($matches); $this->assertEquals(ClientSyncService::MATCH_SCORE_EXACT, $matches[0]['match_score']); $this->assertEquals('vat', $matches[0]['match_type']); } /** * Test batch sync functionality */ public function test_batch_sync_functionality() { // Arrange $client_ids = [100, 101, 102]; foreach ($client_ids as $client_id) { $client_data = $this->test_client_data; $client_data['userid'] = $client_id; $this->mockPerfexClientRetrieval($client_id, $client_data); } // Mock successful API responses $this->api_client_mock->expects($this->exactly(3)) ->method('create_customer') ->willReturn([ 'success' => true, 'data' => ['customer_id' => 999] ]); // Act $result = $this->client_sync->batch_sync_customers($client_ids); // Assert $this->assertIsArray($result); $this->assertEquals(3, $result['total']); $this->assertEquals(3, $result['success']); $this->assertEquals(0, $result['errors']); $this->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 */ public function test_sync_statistics_tracking() { // Arrange - Perform several sync operations $this->setupMultipleSyncOperations(); // Act $stats = $this->client_sync->get_sync_statistics(); // Assert $this->assertIsArray($stats); $this->assertArrayHasKey('total_customers', $stats); $this->assertArrayHasKey('synced_customers', $stats); $this->assertArrayHasKey('pending_customers', $stats); $this->assertArrayHasKey('error_customers', $stats); $this->assertArrayHasKey('last_sync', $stats); } // Helper methods protected function mockPerfexClientRetrieval($client_id, $client_data) { // Mock CodeIgniter instance and clients_model $CI = $this->createMock(stdClass::class); $clients_model = $this->createMock(stdClass::class); $clients_model->expects($this->any()) ->method('get') ->with($client_id) ->willReturn((object)$client_data); // This would need proper CI mock injection in real implementation } protected function mockPerfexClientCreation($expected_client_id) { // Mock successful client creation in Perfex // This would need proper CI mock injection in real implementation } protected function mockModificationTimes() { // Mock modification time retrieval methods $reflection = new ReflectionClass($this->client_sync); $perfex_time_method = $reflection->getMethod('get_perfex_modification_time'); $perfex_time_method->setAccessible(true); $moloni_time_method = $reflection->getMethod('get_moloni_modification_time'); $moloni_time_method->setAccessible(true); // Set times to simulate recent modifications on both sides // Implementation would need proper mocking } protected function setupMultipleSyncOperations() { // Setup multiple test sync operations for statistics testing // This would involve creating multiple mappings and sync logs } protected function tearDown(): void { // Clean up test data $this->cleanupTestData(); parent::tearDown(); } protected function cleanupTestData() { // Remove test mappings and sync logs // This would need proper database cleanup } }