- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
420 lines
17 KiB
PHP
420 lines
17 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DeskMoloni\Tests\Integration;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use DeskMoloni\Tests\TestHelpers;
|
|
|
|
/**
|
|
* Integration Test: Client Synchronization Workflow
|
|
*
|
|
* This test MUST FAIL initially as part of TDD methodology.
|
|
* Tests complete client sync workflow between Perfex CRM and Moloni.
|
|
*
|
|
* @group integration
|
|
* @group client-sync
|
|
*/
|
|
class ClientSyncTest extends TestCase
|
|
{
|
|
private array $testConfig;
|
|
private \PDO $pdo;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
global $testConfig;
|
|
$this->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_%'");
|
|
}
|
|
} |