fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
This commit is contained in:
472
modules/desk_moloni/tests/database/MappingTableTest.php
Normal file
472
modules/desk_moloni/tests/database/MappingTableTest.php
Normal file
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MappingTableTest.php
|
||||
*
|
||||
* PHPUnit tests for desk_moloni_mapping table structure and validation rules
|
||||
* Tests bidirectional entity mapping between Perfex and Moloni
|
||||
*
|
||||
* @package DeskMoloni\Tests\Database
|
||||
* @author Database Design Specialist
|
||||
* @version 3.0
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../tests/TestCase.php');
|
||||
|
||||
class MappingTableTest extends TestCase
|
||||
{
|
||||
private $tableName = 'desk_moloni_mapping';
|
||||
private $testMappingModel;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->clearTestData();
|
||||
|
||||
// Initialize test model (will be implemented after tests)
|
||||
// $this->testMappingModel = new DeskMoloniMapping();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->clearTestData();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test table structure exists with correct columns
|
||||
*/
|
||||
public function testTableStructureExists()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Verify table exists
|
||||
$this->assertTrue($db->table_exists($this->tableName), "Table {$this->tableName} should exist");
|
||||
|
||||
// Verify required columns exist
|
||||
$expectedColumns = [
|
||||
'id', 'entity_type', 'perfex_id', 'moloni_id', 'sync_direction',
|
||||
'last_sync_at', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
foreach ($expectedColumns as $column) {
|
||||
$this->assertTrue($db->field_exists($column, $this->tableName),
|
||||
"Column '{$column}' should exist in {$this->tableName}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test entity_type ENUM values
|
||||
*/
|
||||
public function testEntityTypeEnumValues()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$validEntityTypes = ['client', 'product', 'invoice', 'estimate', 'credit_note'];
|
||||
|
||||
foreach ($validEntityTypes as $entityType) {
|
||||
$data = [
|
||||
'entity_type' => $entityType,
|
||||
'perfex_id' => rand(1, 1000),
|
||||
'moloni_id' => rand(1, 1000),
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data),
|
||||
"Valid entity type '{$entityType}' should insert successfully");
|
||||
|
||||
// Clean up immediately to avoid constraint conflicts
|
||||
$db->where('entity_type', $entityType)
|
||||
->where('perfex_id', $data['perfex_id'])
|
||||
->delete($this->tableName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sync_direction ENUM values
|
||||
*/
|
||||
public function testSyncDirectionEnumValues()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$validDirections = ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'];
|
||||
|
||||
foreach ($validDirections as $direction) {
|
||||
$data = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => rand(1, 1000),
|
||||
'moloni_id' => rand(1, 1000),
|
||||
'sync_direction' => $direction
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data),
|
||||
"Valid sync direction '{$direction}' should insert successfully");
|
||||
|
||||
// Verify stored value
|
||||
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
|
||||
$this->assertEquals($direction, $record->sync_direction, "Sync direction should match inserted value");
|
||||
|
||||
// Clean up
|
||||
$db->where('perfex_id', $data['perfex_id'])->delete($this->tableName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test default sync_direction value
|
||||
*/
|
||||
public function testDefaultSyncDirection()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$data = [
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => rand(1, 1000),
|
||||
'moloni_id' => rand(1, 1000)
|
||||
// sync_direction omitted to test default
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data), 'Insert without sync_direction should succeed');
|
||||
|
||||
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
|
||||
$this->assertEquals('bidirectional', $record->sync_direction, 'Default sync_direction should be bidirectional');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test unique constraint on entity_type + perfex_id
|
||||
*/
|
||||
public function testUniquePerfexMapping()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Insert first record
|
||||
$data1 = [
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => 12345,
|
||||
'moloni_id' => 54321,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data1), 'First mapping insert should succeed');
|
||||
|
||||
// Try to insert duplicate perfex mapping - should fail
|
||||
$data2 = [
|
||||
'entity_type' => 'invoice', // Same entity type
|
||||
'perfex_id' => 12345, // Same perfex ID
|
||||
'moloni_id' => 98765, // Different moloni ID
|
||||
'sync_direction' => 'perfex_to_moloni'
|
||||
];
|
||||
|
||||
$this->assertFalse($db->insert($this->tableName, $data2), 'Duplicate perfex mapping should fail');
|
||||
$this->assertStringContainsString('Duplicate', $db->error()['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test unique constraint on entity_type + moloni_id
|
||||
*/
|
||||
public function testUniqueMoloniMapping()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Insert first record
|
||||
$data1 = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 11111,
|
||||
'moloni_id' => 22222,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data1), 'First mapping insert should succeed');
|
||||
|
||||
// Try to insert duplicate moloni mapping - should fail
|
||||
$data2 = [
|
||||
'entity_type' => 'client', // Same entity type
|
||||
'perfex_id' => 33333, // Different perfex ID
|
||||
'moloni_id' => 22222, // Same moloni ID
|
||||
'sync_direction' => 'moloni_to_perfex'
|
||||
];
|
||||
|
||||
$this->assertFalse($db->insert($this->tableName, $data2), 'Duplicate moloni mapping should fail');
|
||||
$this->assertStringContainsString('Duplicate', $db->error()['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that same IDs can exist for different entity types
|
||||
*/
|
||||
public function testDifferentEntityTypesAllowSameIds()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$sameId = 99999;
|
||||
|
||||
// Insert mappings with same IDs but different entity types
|
||||
$mappings = [
|
||||
['entity_type' => 'client', 'perfex_id' => $sameId, 'moloni_id' => $sameId],
|
||||
['entity_type' => 'product', 'perfex_id' => $sameId, 'moloni_id' => $sameId],
|
||||
['entity_type' => 'invoice', 'perfex_id' => $sameId, 'moloni_id' => $sameId]
|
||||
];
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
$mapping['sync_direction'] = 'bidirectional';
|
||||
$this->assertTrue($db->insert($this->tableName, $mapping),
|
||||
"Same IDs should be allowed for different entity types: {$mapping['entity_type']}");
|
||||
}
|
||||
|
||||
// Verify all records exist
|
||||
$count = $db->where('perfex_id', $sameId)->count_all_results($this->tableName);
|
||||
$this->assertEquals(3, $count, 'Should have 3 mappings with same IDs but different entity types');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test last_sync_at timestamp handling
|
||||
*/
|
||||
public function testLastSyncAtTimestamp()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Insert record without last_sync_at
|
||||
$data = [
|
||||
'entity_type' => 'estimate',
|
||||
'perfex_id' => rand(1, 1000),
|
||||
'moloni_id' => rand(1, 1000),
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data), 'Insert should succeed');
|
||||
|
||||
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
|
||||
$this->assertNull($record->last_sync_at, 'last_sync_at should be NULL initially');
|
||||
|
||||
// Update with sync timestamp
|
||||
$syncTime = date('Y-m-d H:i:s');
|
||||
$db->where('perfex_id', $data['perfex_id'])->update($this->tableName, ['last_sync_at' => $syncTime]);
|
||||
|
||||
$updatedRecord = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
|
||||
$this->assertEquals($syncTime, $updatedRecord->last_sync_at, 'last_sync_at should be updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test performance indexes exist
|
||||
*/
|
||||
public function testPerformanceIndexes()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$query = "SHOW INDEX FROM {$this->tableName}";
|
||||
$indexes = $db->query($query)->result_array();
|
||||
|
||||
$indexNames = array_column($indexes, 'Key_name');
|
||||
|
||||
// Expected indexes
|
||||
$expectedIndexes = [
|
||||
'PRIMARY',
|
||||
'unique_perfex_mapping',
|
||||
'unique_moloni_mapping',
|
||||
'idx_entity_perfex',
|
||||
'idx_entity_moloni',
|
||||
'idx_sync_direction',
|
||||
'idx_last_sync',
|
||||
'idx_created_at'
|
||||
];
|
||||
|
||||
foreach ($expectedIndexes as $expectedIndex) {
|
||||
$this->assertContains($expectedIndex, $indexNames,
|
||||
"Index '{$expectedIndex}' should exist for performance optimization");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bidirectional mapping functionality
|
||||
*/
|
||||
public function testBidirectionalMappingScenarios()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Test Perfex to Moloni sync
|
||||
$perfexToMoloni = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 100,
|
||||
'moloni_id' => 200,
|
||||
'sync_direction' => 'perfex_to_moloni'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $perfexToMoloni),
|
||||
'Perfex to Moloni mapping should insert successfully');
|
||||
|
||||
// Test Moloni to Perfex sync
|
||||
$moloniToPerfex = [
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => 300,
|
||||
'moloni_id' => 400,
|
||||
'sync_direction' => 'moloni_to_perfex'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $moloniToPerfex),
|
||||
'Moloni to Perfex mapping should insert successfully');
|
||||
|
||||
// Test bidirectional sync
|
||||
$bidirectional = [
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => 500,
|
||||
'moloni_id' => 600,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $bidirectional),
|
||||
'Bidirectional mapping should insert successfully');
|
||||
|
||||
// Verify mappings can be retrieved by direction
|
||||
$perfexDirection = $db->where('sync_direction', 'perfex_to_moloni')->count_all_results($this->tableName);
|
||||
$this->assertGreaterThanOrEqual(1, $perfexDirection, 'Should find perfex_to_moloni mappings');
|
||||
|
||||
$moloniDirection = $db->where('sync_direction', 'moloni_to_perfex')->count_all_results($this->tableName);
|
||||
$this->assertGreaterThanOrEqual(1, $moloniDirection, 'Should find moloni_to_perfex mappings');
|
||||
|
||||
$bidirectionalCount = $db->where('sync_direction', 'bidirectional')->count_all_results($this->tableName);
|
||||
$this->assertGreaterThanOrEqual(1, $bidirectionalCount, 'Should find bidirectional mappings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test entity lookup by Perfex ID
|
||||
*/
|
||||
public function testPerfexEntityLookup()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$testMappings = [
|
||||
['entity_type' => 'client', 'perfex_id' => 1001, 'moloni_id' => 2001],
|
||||
['entity_type' => 'product', 'perfex_id' => 1002, 'moloni_id' => 2002],
|
||||
['entity_type' => 'invoice', 'perfex_id' => 1003, 'moloni_id' => 2003]
|
||||
];
|
||||
|
||||
foreach ($testMappings as $mapping) {
|
||||
$mapping['sync_direction'] = 'bidirectional';
|
||||
$db->insert($this->tableName, $mapping);
|
||||
}
|
||||
|
||||
// Test lookup by entity type and perfex ID
|
||||
foreach ($testMappings as $expected) {
|
||||
$result = $db->where('entity_type', $expected['entity_type'])
|
||||
->where('perfex_id', $expected['perfex_id'])
|
||||
->get($this->tableName)
|
||||
->row();
|
||||
|
||||
$this->assertNotNull($result, "Should find mapping for {$expected['entity_type']} with perfex_id {$expected['perfex_id']}");
|
||||
$this->assertEquals($expected['moloni_id'], $result->moloni_id, 'Moloni ID should match');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test entity lookup by Moloni ID
|
||||
*/
|
||||
public function testMoloniEntityLookup()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$testMappings = [
|
||||
['entity_type' => 'estimate', 'perfex_id' => 3001, 'moloni_id' => 4001],
|
||||
['entity_type' => 'credit_note', 'perfex_id' => 3002, 'moloni_id' => 4002]
|
||||
];
|
||||
|
||||
foreach ($testMappings as $mapping) {
|
||||
$mapping['sync_direction'] = 'bidirectional';
|
||||
$db->insert($this->tableName, $mapping);
|
||||
}
|
||||
|
||||
// Test lookup by entity type and moloni ID
|
||||
foreach ($testMappings as $expected) {
|
||||
$result = $db->where('entity_type', $expected['entity_type'])
|
||||
->where('moloni_id', $expected['moloni_id'])
|
||||
->get($this->tableName)
|
||||
->row();
|
||||
|
||||
$this->assertNotNull($result, "Should find mapping for {$expected['entity_type']} with moloni_id {$expected['moloni_id']}");
|
||||
$this->assertEquals($expected['perfex_id'], $result->perfex_id, 'Perfex ID should match');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test timestamp fields auto-population
|
||||
*/
|
||||
public function testTimestampFields()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$beforeInsert = time();
|
||||
|
||||
$data = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => rand(5000, 9999),
|
||||
'moloni_id' => rand(5000, 9999),
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->assertTrue($db->insert($this->tableName, $data), 'Insert should succeed');
|
||||
|
||||
$afterInsert = time();
|
||||
|
||||
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
|
||||
|
||||
// Verify created_at is populated
|
||||
$this->assertNotNull($record->created_at, 'created_at should be auto-populated');
|
||||
$createdTimestamp = strtotime($record->created_at);
|
||||
$this->assertGreaterThanOrEqual($beforeInsert, $createdTimestamp, 'created_at should be recent');
|
||||
$this->assertLessThanOrEqual($afterInsert, $createdTimestamp, 'created_at should not be in future');
|
||||
|
||||
// Verify updated_at is populated
|
||||
$this->assertNotNull($record->updated_at, 'updated_at should be auto-populated');
|
||||
$this->assertEquals($record->created_at, $record->updated_at, 'Initially created_at should equal updated_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clear test data
|
||||
*/
|
||||
private function clearTestData()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
// Clear all test data - using wide range to catch test IDs
|
||||
$db->where('perfex_id >=', 1)
|
||||
->where('perfex_id <=', 9999)
|
||||
->delete($this->tableName);
|
||||
|
||||
$db->where('moloni_id >=', 1)
|
||||
->where('moloni_id <=', 9999)
|
||||
->delete($this->tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test character set and collation
|
||||
*/
|
||||
public function testCharacterSetAndCollation()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$query = "SELECT TABLE_COLLATION
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$this->tableName}'";
|
||||
|
||||
$result = $db->query($query)->row();
|
||||
|
||||
$this->assertEquals('utf8mb4_unicode_ci', $result->TABLE_COLLATION,
|
||||
'Table should use utf8mb4_unicode_ci collation for proper Unicode support');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test storage engine
|
||||
*/
|
||||
public function testStorageEngine()
|
||||
{
|
||||
$db = $this->ci->db;
|
||||
|
||||
$query = "SELECT ENGINE
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$this->tableName}'";
|
||||
|
||||
$result = $db->query($query)->row();
|
||||
|
||||
$this->assertEquals('InnoDB', $result->ENGINE,
|
||||
'Table should use InnoDB engine for ACID compliance and foreign key support');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user